Skip to content

Commit 3e4c6c3

Browse files
committed
87959e9 feat(testing): Use NgZone in TestComponentBuilder.
1 parent ed83584 commit 3e4c6c3

File tree

8 files changed

+407
-52
lines changed

8 files changed

+407
-52
lines changed

BUILD_INFO

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
Thu Apr 28 21:20:04 UTC 2016
2-
ac55e1e27bdebbafda7bee60d4651d57636ce80a
1+
Thu Apr 28 22:48:35 UTC 2016
2+
87959e969b3ddb324f3cd95bf7ee1e3d5131536a

lib/platform/testing/browser_static.dart

+10-2
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,24 @@ import "package:angular2/src/mock/ng_zone_mock.dart" show MockNgZone;
2727
import "package:angular2/src/platform/browser/xhr_impl.dart" show XHRImpl;
2828
import "package:angular2/compiler.dart" show XHR;
2929
import "package:angular2/src/testing/test_component_builder.dart"
30-
show TestComponentBuilder;
30+
show
31+
TestComponentBuilder,
32+
ComponentFixtureAutoDetect,
33+
ComponentFixtureNoNgZone;
3134
import "package:angular2/src/testing/utils.dart" show BrowserDetection;
3235
import "package:angular2/platform/common_dom.dart" show ELEMENT_PROBE_PROVIDERS;
36+
import "package:angular2/src/facade/lang.dart" show IS_DART;
3337
import "package:angular2/src/testing/utils.dart" show Log;
3438

3539
initBrowserTests() {
3640
BrowserDomAdapter.makeCurrent();
3741
BrowserDetection.setup();
3842
}
3943

44+
NgZone createNgZone() {
45+
return IS_DART ? new MockNgZone() : new NgZone(enableLongStackTrace: true);
46+
}
47+
4048
/**
4149
* Default platform providers for testing without a compiler.
4250
*/
@@ -51,7 +59,7 @@ const List<dynamic> ADDITIONAL_TEST_BROWSER_PROVIDERS = const [
5159
const Provider(ViewResolver, useClass: MockViewResolver),
5260
Log,
5361
TestComponentBuilder,
54-
const Provider(NgZone, useClass: MockNgZone),
62+
const Provider(NgZone, useFactory: createNgZone),
5563
const Provider(LocationStrategy, useClass: MockLocationStrategy),
5664
const Provider(AnimationBuilder, useClass: MockAnimationBuilder)
5765
];

lib/src/testing/test_component_builder.dart

+164-38
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ library angular2.src.testing.test_component_builder;
33
import "dart:async";
44
import "package:angular2/core.dart"
55
show
6+
OpaqueToken,
67
ComponentRef,
78
DynamicComponentLoader,
89
Injector,
@@ -11,10 +12,15 @@ import "package:angular2/core.dart"
1112
ElementRef,
1213
EmbeddedViewRef,
1314
ChangeDetectorRef,
14-
provide;
15+
provide,
16+
NgZone,
17+
NgZoneError;
1518
import "package:angular2/compiler.dart" show DirectiveResolver, ViewResolver;
16-
import "package:angular2/src/facade/lang.dart" show Type, isPresent, isBlank;
17-
import "package:angular2/src/facade/async.dart" show PromiseWrapper;
19+
import "package:angular2/src/facade/exceptions.dart" show BaseException;
20+
import "package:angular2/src/facade/lang.dart"
21+
show Type, isPresent, isBlank, IS_DART;
22+
import "package:angular2/src/facade/async.dart"
23+
show PromiseWrapper, ObservableWrapper, PromiseCompleter;
1824
import "package:angular2/src/facade/collection.dart"
1925
show ListWrapper, MapWrapper;
2026
import "utils.dart" show el;
@@ -24,6 +30,9 @@ import "package:angular2/src/core/debug/debug_node.dart"
2430
show DebugNode, DebugElement, getDebugNode;
2531
import "fake_async.dart" show tick;
2632

33+
var ComponentFixtureAutoDetect = new OpaqueToken("ComponentFixtureAutoDetect");
34+
var ComponentFixtureNoNgZone = new OpaqueToken("ComponentFixtureNoNgZone");
35+
2736
/**
2837
* Fixture for debugging and testing a component.
2938
*/
@@ -52,34 +61,144 @@ class ComponentFixture {
5261
* The ChangeDetectorRef for the component
5362
*/
5463
ChangeDetectorRef changeDetectorRef;
55-
ComponentFixture(ComponentRef componentRef) {
64+
/**
65+
* The NgZone in which this component was instantiated.
66+
*/
67+
NgZone ngZone;
68+
bool _autoDetect;
69+
bool _isStable = true;
70+
PromiseCompleter<dynamic> _completer = null;
71+
var _onUnstableSubscription = null;
72+
var _onStableSubscription = null;
73+
var _onMicrotaskEmptySubscription = null;
74+
var _onErrorSubscription = null;
75+
ComponentFixture(ComponentRef componentRef, NgZone ngZone, bool autoDetect) {
5676
this.changeDetectorRef = componentRef.changeDetectorRef;
5777
this.elementRef = componentRef.location;
5878
this.debugElement =
5979
(getDebugNode(this.elementRef.nativeElement) as DebugElement);
6080
this.componentInstance = componentRef.instance;
6181
this.nativeElement = this.elementRef.nativeElement;
6282
this.componentRef = componentRef;
83+
this.ngZone = ngZone;
84+
this._autoDetect = autoDetect;
85+
if (ngZone != null) {
86+
this._onUnstableSubscription =
87+
ObservableWrapper.subscribe(ngZone.onUnstable, (_) {
88+
this._isStable = false;
89+
});
90+
this._onMicrotaskEmptySubscription =
91+
ObservableWrapper.subscribe(ngZone.onMicrotaskEmpty, (_) {
92+
if (this._autoDetect) {
93+
// Do a change detection run with checkNoChanges set to true to check
94+
95+
// there are no changes on the second run.
96+
this.detectChanges(true);
97+
}
98+
});
99+
this._onStableSubscription =
100+
ObservableWrapper.subscribe(ngZone.onStable, (_) {
101+
this._isStable = true;
102+
if (this._completer != null) {
103+
this._completer.resolve(true);
104+
this._completer = null;
105+
}
106+
});
107+
this._onErrorSubscription =
108+
ObservableWrapper.subscribe(ngZone.onError, (NgZoneError error) {
109+
throw error.error;
110+
});
111+
}
63112
}
113+
_tick(bool checkNoChanges) {
114+
this.changeDetectorRef.detectChanges();
115+
if (checkNoChanges) {
116+
this.checkNoChanges();
117+
}
118+
}
119+
64120
/**
65121
* Trigger a change detection cycle for the component.
66122
*/
67123
void detectChanges([bool checkNoChanges = true]) {
68-
this.changeDetectorRef.detectChanges();
69-
if (checkNoChanges) {
70-
this.checkNoChanges();
124+
if (this.ngZone != null) {
125+
// Run the change detection inside the NgZone so that any async tasks as part of the change
126+
127+
// detection are captured by the zone and can be waited for in isStable.
128+
this.ngZone.run(() {
129+
this._tick(checkNoChanges);
130+
});
131+
} else {
132+
// Running without zone. Just do the change detection.
133+
this._tick(checkNoChanges);
71134
}
72135
}
73136

137+
/**
138+
* Do a change detection run to make sure there were no changes.
139+
*/
74140
void checkNoChanges() {
75141
this.changeDetectorRef.checkNoChanges();
76142
}
77143

144+
/**
145+
* Set whether the fixture should autodetect changes.
146+
*
147+
* Also runs detectChanges once so that any existing change is detected.
148+
*/
149+
autoDetectChanges([bool autoDetect = true]) {
150+
if (this.ngZone == null) {
151+
throw new BaseException(
152+
"Cannot call autoDetectChanges when ComponentFixtureNoNgZone is set");
153+
}
154+
this._autoDetect = autoDetect;
155+
this.detectChanges();
156+
}
157+
158+
/**
159+
* Return whether the fixture is currently stable or has async tasks that have not been completed
160+
* yet.
161+
*/
162+
bool isStable() {
163+
return this._isStable;
164+
}
165+
166+
/**
167+
* Get a promise that resolves when the fixture is stable.
168+
*
169+
* This can be used to resume testing after events have triggered asynchronous activity or
170+
* asynchronous change detection.
171+
*/
172+
Future<dynamic> whenStable() {
173+
if (this._isStable) {
174+
return PromiseWrapper.resolve(false);
175+
} else {
176+
this._completer = new PromiseCompleter<dynamic>();
177+
return this._completer.promise;
178+
}
179+
}
180+
78181
/**
79182
* Trigger component destruction.
80183
*/
81184
void destroy() {
82185
this.componentRef.destroy();
186+
if (this._onUnstableSubscription != null) {
187+
ObservableWrapper.dispose(this._onUnstableSubscription);
188+
this._onUnstableSubscription = null;
189+
}
190+
if (this._onStableSubscription != null) {
191+
ObservableWrapper.dispose(this._onStableSubscription);
192+
this._onStableSubscription = null;
193+
}
194+
if (this._onMicrotaskEmptySubscription != null) {
195+
ObservableWrapper.dispose(this._onMicrotaskEmptySubscription);
196+
this._onMicrotaskEmptySubscription = null;
197+
}
198+
if (this._onErrorSubscription != null) {
199+
ObservableWrapper.dispose(this._onErrorSubscription);
200+
this._onErrorSubscription = null;
201+
}
83202
}
84203
}
85204

@@ -228,38 +347,45 @@ class TestComponentBuilder {
228347
*
229348
*/
230349
Future<ComponentFixture> createAsync(Type rootComponentType) {
231-
var mockDirectiveResolver = this._injector.get(DirectiveResolver);
232-
var mockViewResolver = this._injector.get(ViewResolver);
233-
this
234-
._viewOverrides
235-
.forEach((type, view) => mockViewResolver.setView(type, view));
236-
this._templateOverrides.forEach(
237-
(type, template) => mockViewResolver.setInlineTemplate(type, template));
238-
this._directiveOverrides.forEach((component, overrides) {
239-
overrides.forEach((from, to) {
240-
mockViewResolver.overrideViewDirective(component, from, to);
350+
var noNgZone =
351+
IS_DART || this._injector.get(ComponentFixtureNoNgZone, false);
352+
NgZone ngZone = noNgZone ? null : this._injector.get(NgZone, null);
353+
bool autoDetect = this._injector.get(ComponentFixtureAutoDetect, false);
354+
var initComponent = () {
355+
var mockDirectiveResolver = this._injector.get(DirectiveResolver);
356+
var mockViewResolver = this._injector.get(ViewResolver);
357+
this
358+
._viewOverrides
359+
.forEach((type, view) => mockViewResolver.setView(type, view));
360+
this._templateOverrides.forEach((type, template) =>
361+
mockViewResolver.setInlineTemplate(type, template));
362+
this._directiveOverrides.forEach((component, overrides) {
363+
overrides.forEach((from, to) {
364+
mockViewResolver.overrideViewDirective(component, from, to);
365+
});
241366
});
242-
});
243-
this._bindingsOverrides.forEach((type, bindings) =>
244-
mockDirectiveResolver.setBindingsOverride(type, bindings));
245-
this._viewBindingsOverrides.forEach((type, bindings) =>
246-
mockDirectiveResolver.setViewBindingsOverride(type, bindings));
247-
var rootElId = '''root${ _nextRootElementId ++}''';
248-
var rootEl = el('''<div id="${ rootElId}"></div>''');
249-
var doc = this._injector.get(DOCUMENT);
250-
// TODO(juliemr): can/should this be optional?
251-
var oldRoots = DOM.querySelectorAll(doc, "[id^=root]");
252-
for (var i = 0; i < oldRoots.length; i++) {
253-
DOM.remove(oldRoots[i]);
254-
}
255-
DOM.appendChild(doc.body, rootEl);
256-
Future<ComponentRef> promise = this
257-
._injector
258-
.get(DynamicComponentLoader)
259-
.loadAsRoot(rootComponentType, '''#${ rootElId}''', this._injector);
260-
return promise.then((componentRef) {
261-
return new ComponentFixture(componentRef);
262-
});
367+
this._bindingsOverrides.forEach((type, bindings) =>
368+
mockDirectiveResolver.setBindingsOverride(type, bindings));
369+
this._viewBindingsOverrides.forEach((type, bindings) =>
370+
mockDirectiveResolver.setViewBindingsOverride(type, bindings));
371+
var rootElId = '''root${ _nextRootElementId ++}''';
372+
var rootEl = el('''<div id="${ rootElId}"></div>''');
373+
var doc = this._injector.get(DOCUMENT);
374+
// TODO(juliemr): can/should this be optional?
375+
var oldRoots = DOM.querySelectorAll(doc, "[id^=root]");
376+
for (var i = 0; i < oldRoots.length; i++) {
377+
DOM.remove(oldRoots[i]);
378+
}
379+
DOM.appendChild(doc.body, rootEl);
380+
Future<ComponentRef> promise = this
381+
._injector
382+
.get(DynamicComponentLoader)
383+
.loadAsRoot(rootComponentType, '''#${ rootElId}''', this._injector);
384+
return promise.then((componentRef) {
385+
return new ComponentFixture(componentRef, ngZone, autoDetect);
386+
});
387+
};
388+
return ngZone == null ? initComponent() : ngZone.run(initComponent);
263389
}
264390

265391
ComponentFixture createFakeAsync(Type rootComponentType) {

lib/src/testing/test_injector.dart

+7-2
Original file line numberDiff line numberDiff line change
@@ -166,15 +166,15 @@ class InjectSetupWrapper {
166166
Function inject(List<dynamic> tokens, Function fn) {
167167
return () {
168168
this._addProviders();
169-
return inject(tokens, fn)();
169+
return inject_impl(tokens, fn)();
170170
};
171171
}
172172

173173
/** @Deprecated {use async(withProviders().inject())} */
174174
Function injectAsync(List<dynamic> tokens, Function fn) {
175175
return () {
176176
this._addProviders();
177-
return injectAsync(tokens, fn)();
177+
return injectAsync_impl(tokens, fn)();
178178
};
179179
}
180180
}
@@ -206,3 +206,8 @@ withProviders(dynamic /* () => any */ providers) {
206206
Function injectAsync(List<dynamic> tokens, Function fn) {
207207
return async(inject(tokens, fn));
208208
}
209+
// This is to ensure inject(Async) within InjectSetupWrapper doesn't call itself
210+
211+
// when transpiled to Dart.
212+
var inject_impl = inject;
213+
var injectAsync_impl = injectAsync;

lib/testing.dart

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ library angular2.testing;
1010

1111
export "src/testing/testing.dart";
1212
export "src/testing/test_component_builder.dart"
13-
show ComponentFixture, TestComponentBuilder;
13+
show
14+
ComponentFixture,
15+
TestComponentBuilder,
16+
ComponentFixtureAutoDetect,
17+
ComponentFixtureNoNgZone;
1418
export "src/testing/test_injector.dart";
1519
export "src/testing/fake_async.dart";
1620
export "package:angular2/src/mock/view_resolver_mock.dart"

test/core/linker/dynamic_component_loader_spec.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ main() {
166166
var rootEl = createRootElement(doc, "child-cmp");
167167
DOM.appendChild(doc.body, rootEl);
168168
loader.loadAsRoot(ChildComp, null, injector).then((componentRef) {
169-
var el = new ComponentFixture(componentRef);
169+
var el = new ComponentFixture(componentRef, null, false);
170170
expect(rootEl.parentNode).toBe(doc.body);
171171
el.detectChanges();
172172
expect(rootEl).toHaveText("hello");

0 commit comments

Comments
 (0)