Skip to content

Commit 769835e

Browse files
committed
feat(testing): Use NgZone in TestComponentBuilder.
Instantiating the test component within an NgZone will let us track async tasks in event handlers and change detection. We can also do auto change detection when triggering events through dispatchEvent and not have to do fixture.detectChange() manually in the test. New API: ComponentFixture.autoDetectChanges() - This puts the fixture in auto detect mode that automatically calls detectChanges when the microtask queue is empty (Similar to how change detection is triggered in an actual application). ComponentFixture.isStable() - This returns a boolean whether the fixture is currently stable or has some async tasks that need to be completed. ComponentFixture.whenStable() - This returns a promise that is resolved when the fixture is stable after all async tasks are complete. Closes angular#8301
1 parent ac55e1e commit 769835e

File tree

8 files changed

+441
-76
lines changed

8 files changed

+441
-76
lines changed

modules/angular2/platform/testing/browser_static.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@ import {MockNgZone} from 'angular2/src/mock/ng_zone_mock';
2020
import {XHRImpl} from "angular2/src/platform/browser/xhr_impl";
2121
import {XHR} from 'angular2/compiler';
2222

23-
import {TestComponentBuilder} from 'angular2/src/testing/test_component_builder';
23+
import {
24+
TestComponentBuilder,
25+
ComponentFixtureAutoDetect,
26+
ComponentFixtureNoNgZone
27+
} from 'angular2/src/testing/test_component_builder';
2428

2529
import {BrowserDetection} from 'angular2/src/testing/utils';
2630

2731
import {ELEMENT_PROBE_PROVIDERS} from 'angular2/platform/common_dom';
2832

29-
import {CONST_EXPR} from 'angular2/src/facade/lang';
33+
import {CONST_EXPR, IS_DART} from 'angular2/src/facade/lang';
3034

3135
import {Log} from 'angular2/src/testing/utils';
3236

@@ -35,6 +39,10 @@ function initBrowserTests() {
3539
BrowserDetection.setup();
3640
}
3741

42+
function createNgZone(): NgZone {
43+
return IS_DART ? new MockNgZone() : new NgZone({enableLongStackTrace: true});
44+
}
45+
3846
/**
3947
* Default platform providers for testing without a compiler.
4048
*/
@@ -52,7 +60,7 @@ export const ADDITIONAL_TEST_BROWSER_PROVIDERS: Array<any /*Type | Provider | an
5260
new Provider(ViewResolver, {useClass: MockViewResolver}),
5361
Log,
5462
TestComponentBuilder,
55-
new Provider(NgZone, {useClass: MockNgZone}),
63+
new Provider(NgZone, {useFactory: createNgZone}),
5664
new Provider(LocationStrategy, {useClass: MockLocationStrategy}),
5765
new Provider(AnimationBuilder, {useClass: MockAnimationBuilder}),
5866
]);

modules/angular2/platform/testing/server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {MockViewResolver} from 'angular2/src/mock/view_resolver_mock';
1818
import {MockLocationStrategy} from 'angular2/src/mock/mock_location_strategy';
1919
import {MockNgZone} from 'angular2/src/mock/ng_zone_mock';
2020

21+
import {createNgZone} from 'angular2/src/core/application_ref';
2122
import {TestComponentBuilder} from 'angular2/src/testing/test_component_builder';
2223
import {XHR} from 'angular2/src/compiler/xhr';
2324
import {BrowserDetection} from 'angular2/src/testing/utils';
@@ -85,7 +86,7 @@ export const TEST_SERVER_APPLICATION_PROVIDERS: Array<any /*Type | Provider | an
8586
new Provider(ViewResolver, {useClass: MockViewResolver}),
8687
Log,
8788
TestComponentBuilder,
88-
new Provider(NgZone, {useClass: MockNgZone}),
89+
new Provider(NgZone, {useFactory: createNgZone}),
8990
new Provider(LocationStrategy, {useClass: MockLocationStrategy}),
9091
new Provider(AnimationBuilder, {useClass: MockAnimationBuilder}),
9192
]);

modules/angular2/src/testing/test_component_builder.ts

Lines changed: 167 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
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';
1215
import {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';
1620
import {ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
1721

1822
import {el} from './utils';
@@ -24,6 +28,9 @@ import {DebugNode, DebugElement, getDebugNode} from 'angular2/src/core/debug/deb
2428

2529
import {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

88200
var _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();

modules/angular2/src/testing/test_injector.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,15 +157,15 @@ export class InjectSetupWrapper {
157157
inject(tokens: any[], fn: Function): Function {
158158
return () => {
159159
this._addProviders();
160-
return inject(tokens, fn)();
160+
return inject_impl(tokens, fn)();
161161
}
162162
}
163163

164164
/** @Deprecated {use async(withProviders().inject())} */
165165
injectAsync(tokens: any[], fn: Function): Function {
166166
return () => {
167167
this._addProviders();
168-
return injectAsync(tokens, fn)();
168+
return injectAsync_impl(tokens, fn)();
169169
}
170170
}
171171
}
@@ -197,3 +197,8 @@ export function withProviders(providers: () => any) {
197197
export function injectAsync(tokens: any[], fn: Function): Function {
198198
return async(inject(tokens, fn));
199199
}
200+
201+
// This is to ensure inject(Async) within InjectSetupWrapper doesn't call itself
202+
// when transpiled to Dart.
203+
var inject_impl = inject;
204+
var injectAsync_impl = injectAsync;

0 commit comments

Comments
 (0)