11import {
2+ OpaqueToken ,
23 ComponentRef ,
34 DynamicComponentLoader ,
45 Injector ,
@@ -7,12 +8,15 @@ import {
78 ElementRef ,
89 EmbeddedViewRef ,
910 ChangeDetectorRef ,
10- provide
11+ provide ,
12+ NgZone ,
13+ NgZoneError
1114} from 'angular2/core' ;
1215import { DirectiveResolver , ViewResolver } from 'angular2/compiler' ;
1316
14- import { Type , isPresent , isBlank } from 'angular2/src/facade/lang' ;
15- import { PromiseWrapper } from 'angular2/src/facade/async' ;
17+ import { BaseException } from 'angular2/src/facade/exceptions' ;
18+ import { Type , isPresent , isBlank , IS_DART } from 'angular2/src/facade/lang' ;
19+ import { PromiseWrapper , ObservableWrapper , PromiseCompleter } from 'angular2/src/facade/async' ;
1620import { ListWrapper , MapWrapper } from 'angular2/src/facade/collection' ;
1721
1822import { el } from './utils' ;
@@ -24,6 +28,9 @@ import {DebugNode, DebugElement, getDebugNode} from 'angular2/src/core/debug/deb
2428
2529import { tick } from './fake_async' ;
2630
31+ export var ComponentFixtureAutoDetect = new OpaqueToken ( "ComponentFixtureAutoDetect" ) ;
32+ export var ComponentFixtureNoNgZone = new OpaqueToken ( "ComponentFixtureNoNgZone" ) ;
33+
2734/**
2835 * Fixture for debugging and testing a component.
2936 */
@@ -58,31 +65,136 @@ export class ComponentFixture {
5865 */
5966 changeDetectorRef : ChangeDetectorRef ;
6067
61- constructor ( componentRef : ComponentRef ) {
68+ /**
69+ * The NgZone in which this component was instantiated.
70+ */
71+ ngZone : NgZone ;
72+
73+ private _autoDetect : boolean ;
74+
75+ private _isStable : boolean = true ;
76+ private _completer : PromiseCompleter < any > = null ;
77+ private _onUnstableSubscription = null ;
78+ private _onStableSubscription = null ;
79+ private _onMicrotaskEmptySubscription = null ;
80+ private _onErrorSubscription = null ;
81+
82+ constructor ( componentRef : ComponentRef , ngZone : NgZone , autoDetect : boolean ) {
6283 this . changeDetectorRef = componentRef . changeDetectorRef ;
6384 this . elementRef = componentRef . location ;
6485 this . debugElement = < DebugElement > getDebugNode ( this . elementRef . nativeElement ) ;
6586 this . componentInstance = componentRef . instance ;
6687 this . nativeElement = this . elementRef . nativeElement ;
6788 this . componentRef = componentRef ;
89+ this . ngZone = ngZone ;
90+ this . _autoDetect = autoDetect ;
91+
92+ if ( ngZone != null ) {
93+ this . _onUnstableSubscription =
94+ ObservableWrapper . subscribe ( ngZone . onUnstable , ( _ ) => { this . _isStable = false ; } ) ;
95+ this . _onMicrotaskEmptySubscription =
96+ ObservableWrapper . subscribe ( ngZone . onMicrotaskEmpty , ( _ ) => {
97+ if ( this . _autoDetect ) {
98+ // Do a change detection run with checkNoChanges set to true to check
99+ // there are no changes on the second run.
100+ this . detectChanges ( true ) ;
101+ }
102+ } ) ;
103+ this . _onStableSubscription = ObservableWrapper . subscribe ( ngZone . onStable , ( _ ) => {
104+ this . _isStable = true ;
105+ if ( this . _completer != null ) {
106+ this . _completer . resolve ( true ) ;
107+ this . _completer = null ;
108+ }
109+ } ) ;
110+
111+ this . _onErrorSubscription = ObservableWrapper . subscribe (
112+ ngZone . onError , ( error : NgZoneError ) => { throw error . error ; } ) ;
113+ }
114+ }
115+
116+ private _tick ( checkNoChanges : boolean ) {
117+ this . changeDetectorRef . detectChanges ( ) ;
118+ if ( checkNoChanges ) {
119+ this . checkNoChanges ( ) ;
120+ }
68121 }
69122
70123 /**
71124 * Trigger a change detection cycle for the component.
72125 */
73126 detectChanges ( checkNoChanges : boolean = true ) : void {
74- this . changeDetectorRef . detectChanges ( ) ;
75- if ( checkNoChanges ) {
76- this . checkNoChanges ( ) ;
127+ if ( this . ngZone != null ) {
128+ // Run the change detection inside the NgZone so that any async tasks as part of the change
129+ // detection are captured by the zone and can be waited for in isStable.
130+ this . ngZone . run ( ( ) => { this . _tick ( checkNoChanges ) ; } ) ;
131+ } else {
132+ // Running without zone. Just do the change detection.
133+ this . _tick ( checkNoChanges ) ;
77134 }
78135 }
79136
137+ /**
138+ * Do a change detection run to make sure there were no changes.
139+ */
80140 checkNoChanges ( ) : void { this . changeDetectorRef . checkNoChanges ( ) ; }
81141
142+ /**
143+ * Set whether the fixture should autodetect changes.
144+ *
145+ * Also runs detectChanges once so that any existing change is detected.
146+ */
147+ autoDetectChanges ( autoDetect : boolean = true ) {
148+ if ( this . ngZone == null ) {
149+ throw new BaseException ( 'Cannot call autoDetectChanges when ComponentFixtureNoNgZone is set' ) ;
150+ }
151+ this . _autoDetect = autoDetect ;
152+ this . detectChanges ( ) ;
153+ }
154+
155+ /**
156+ * Return whether the fixture is currently stable or has async tasks that have not been completed
157+ * yet.
158+ */
159+ isStable ( ) : boolean { return this . _isStable ; }
160+
161+ /**
162+ * Get a promise that resolves when the fixture is stable.
163+ *
164+ * This can be used to resume testing after events have triggered asynchronous activity or
165+ * asynchronous change detection.
166+ */
167+ whenStable ( ) : Promise < any > {
168+ if ( this . _isStable ) {
169+ return PromiseWrapper . resolve ( false ) ;
170+ } else {
171+ this . _completer = new PromiseCompleter < any > ( ) ;
172+ return this . _completer . promise ;
173+ }
174+ }
175+
82176 /**
83177 * Trigger component destruction.
84178 */
85- destroy ( ) : void { this . componentRef . destroy ( ) ; }
179+ destroy ( ) : void {
180+ this . componentRef . destroy ( ) ;
181+ if ( this . _onUnstableSubscription != null ) {
182+ ObservableWrapper . dispose ( this . _onUnstableSubscription ) ;
183+ this . _onUnstableSubscription = null ;
184+ }
185+ if ( this . _onStableSubscription != null ) {
186+ ObservableWrapper . dispose ( this . _onStableSubscription ) ;
187+ this . _onStableSubscription = null ;
188+ }
189+ if ( this . _onMicrotaskEmptySubscription != null ) {
190+ ObservableWrapper . dispose ( this . _onMicrotaskEmptySubscription ) ;
191+ this . _onMicrotaskEmptySubscription = null ;
192+ }
193+ if ( this . _onErrorSubscription != null ) {
194+ ObservableWrapper . dispose ( this . _onErrorSubscription ) ;
195+ this . _onErrorSubscription = null ;
196+ }
197+ }
86198}
87199
88200var _nextRootElementId = 0 ;
@@ -108,7 +220,7 @@ export class TestComponentBuilder {
108220
109221 /** @internal */
110222 _clone ( ) : TestComponentBuilder {
111- var clone = new TestComponentBuilder ( this . _injector ) ;
223+ let clone = new TestComponentBuilder ( this . _injector ) ;
112224 clone . _viewOverrides = MapWrapper . clone ( this . _viewOverrides ) ;
113225 clone . _directiveOverrides = MapWrapper . clone ( this . _directiveOverrides ) ;
114226 clone . _templateOverrides = MapWrapper . clone ( this . _templateOverrides ) ;
@@ -127,7 +239,7 @@ export class TestComponentBuilder {
127239 * @return {TestComponentBuilder }
128240 */
129241 overrideTemplate ( componentType : Type , template : string ) : TestComponentBuilder {
130- var clone = this . _clone ( ) ;
242+ let clone = this . _clone ( ) ;
131243 clone . _templateOverrides . set ( componentType , template ) ;
132244 return clone ;
133245 }
@@ -141,7 +253,7 @@ export class TestComponentBuilder {
141253 * @return {TestComponentBuilder }
142254 */
143255 overrideView ( componentType : Type , view : ViewMetadata ) : TestComponentBuilder {
144- var clone = this . _clone ( ) ;
256+ let clone = this . _clone ( ) ;
145257 clone . _viewOverrides . set ( componentType , view ) ;
146258 return clone ;
147259 }
@@ -156,8 +268,8 @@ export class TestComponentBuilder {
156268 * @return {TestComponentBuilder }
157269 */
158270 overrideDirective ( componentType : Type , from : Type , to : Type ) : TestComponentBuilder {
159- var clone = this . _clone ( ) ;
160- var overridesForComponent = clone . _directiveOverrides . get ( componentType ) ;
271+ let clone = this . _clone ( ) ;
272+ let overridesForComponent = clone . _directiveOverrides . get ( componentType ) ;
161273 if ( ! isPresent ( overridesForComponent ) ) {
162274 clone . _directiveOverrides . set ( componentType , new Map < Type , Type > ( ) ) ;
163275 overridesForComponent = clone . _directiveOverrides . get ( componentType ) ;
@@ -182,7 +294,7 @@ export class TestComponentBuilder {
182294 * @return {TestComponentBuilder }
183295 */
184296 overrideProviders ( type : Type , providers : any [ ] ) : TestComponentBuilder {
185- var clone = this . _clone ( ) ;
297+ let clone = this . _clone ( ) ;
186298 clone . _bindingsOverrides . set ( type , providers ) ;
187299 return clone ;
188300 }
@@ -210,7 +322,7 @@ export class TestComponentBuilder {
210322 * @return {TestComponentBuilder }
211323 */
212324 overrideViewProviders ( type : Type , providers : any [ ] ) : TestComponentBuilder {
213- var clone = this . _clone ( ) ;
325+ let clone = this . _clone ( ) ;
214326 clone . _viewBindingsOverrides . set ( type , providers ) ;
215327 return clone ;
216328 }
@@ -228,40 +340,49 @@ export class TestComponentBuilder {
228340 * @return {Promise<ComponentFixture> }
229341 */
230342 createAsync ( rootComponentType : Type ) : Promise < ComponentFixture > {
231- var mockDirectiveResolver = this . _injector . get ( DirectiveResolver ) ;
232- var mockViewResolver = this . _injector . get ( ViewResolver ) ;
233- this . _viewOverrides . forEach ( ( view , type ) => mockViewResolver . setView ( type , view ) ) ;
234- this . _templateOverrides . forEach ( ( template , type ) =>
235- mockViewResolver . setInlineTemplate ( type , template ) ) ;
236- this . _directiveOverrides . forEach ( ( overrides , component ) => {
237- overrides . forEach (
238- ( to , from ) => { mockViewResolver . overrideViewDirective ( component , from , to ) ; } ) ;
239- } ) ;
240- this . _bindingsOverrides . forEach ( ( bindings , type ) =>
241- mockDirectiveResolver . setBindingsOverride ( type , bindings ) ) ;
242- this . _viewBindingsOverrides . forEach (
243- ( bindings , type ) => mockDirectiveResolver . setViewBindingsOverride ( type , bindings ) ) ;
244-
245- var rootElId = `root${ _nextRootElementId ++ } ` ;
246- var rootEl = el ( `<div id="${ rootElId } "></div>` ) ;
247- var doc = this . _injector . get ( DOCUMENT ) ;
248-
249- // TODO(juliemr): can/should this be optional?
250- var oldRoots = DOM . querySelectorAll ( doc , '[id^=root]' ) ;
251- for ( var i = 0 ; i < oldRoots . length ; i ++ ) {
252- DOM . remove ( oldRoots [ i ] ) ;
253- }
254- DOM . appendChild ( doc . body , rootEl ) ;
255-
256- var promise : Promise < ComponentRef > =
257- this . _injector . get ( DynamicComponentLoader )
258- . loadAsRoot ( rootComponentType , `#${ rootElId } ` , this . _injector ) ;
259- return promise . then ( ( componentRef ) => { return new ComponentFixture ( componentRef ) ; } ) ;
343+ let noNgZone = IS_DART || this . _injector . get ( ComponentFixtureNoNgZone , false ) ;
344+ let ngZone : NgZone = noNgZone ? null : this . _injector . get ( NgZone , null ) ;
345+ let autoDetect : boolean = this . _injector . get ( ComponentFixtureAutoDetect , false ) ;
346+
347+ let initComponent = ( ) => {
348+ let mockDirectiveResolver = this . _injector . get ( DirectiveResolver ) ;
349+ let mockViewResolver = this . _injector . get ( ViewResolver ) ;
350+ this . _viewOverrides . forEach ( ( view , type ) => mockViewResolver . setView ( type , view ) ) ;
351+ this . _templateOverrides . forEach ( ( template , type ) =>
352+ mockViewResolver . setInlineTemplate ( type , template ) ) ;
353+ this . _directiveOverrides . forEach ( ( overrides , component ) => {
354+ overrides . forEach (
355+ ( to , from ) => { mockViewResolver . overrideViewDirective ( component , from , to ) ; } ) ;
356+ } ) ;
357+ this . _bindingsOverrides . forEach (
358+ ( bindings , type ) => mockDirectiveResolver . setBindingsOverride ( type , bindings ) ) ;
359+ this . _viewBindingsOverrides . forEach (
360+ ( bindings , type ) => mockDirectiveResolver . setViewBindingsOverride ( type , bindings ) ) ;
361+
362+ let rootElId = `root${ _nextRootElementId ++ } ` ;
363+ let rootEl = el ( `<div id="${ rootElId } "></div>` ) ;
364+ let doc = this . _injector . get ( DOCUMENT ) ;
365+
366+ // TODO(juliemr): can/should this be optional?
367+ let oldRoots = DOM . querySelectorAll ( doc , '[id^=root]' ) ;
368+ for ( let i = 0 ; i < oldRoots . length ; i ++ ) {
369+ DOM . remove ( oldRoots [ i ] ) ;
370+ }
371+ DOM . appendChild ( doc . body , rootEl ) ;
372+
373+ let promise : Promise < ComponentRef > =
374+ this . _injector . get ( DynamicComponentLoader )
375+ . loadAsRoot ( rootComponentType , `#${ rootElId } ` , this . _injector ) ;
376+ return promise . then (
377+ ( componentRef ) => { return new ComponentFixture ( componentRef , ngZone , autoDetect ) ; } ) ;
378+ } ;
379+
380+ return ngZone == null ? initComponent ( ) : ngZone . run ( initComponent ) ;
260381 }
261382
262383 createFakeAsync ( rootComponentType : Type ) : ComponentFixture {
263- var result ;
264- var error ;
384+ let result ;
385+ let error ;
265386 PromiseWrapper . then ( this . createAsync ( rootComponentType ) , ( _result ) => { result = _result ; } ,
266387 ( _error ) => { error = _error ; } ) ;
267388 tick ( ) ;
0 commit comments