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 #8301
This commit is contained in:

committed by
vikerman

parent
ac55e1e27b
commit
769835e53e
@ -1,4 +1,5 @@
|
||||
import {
|
||||
OpaqueToken,
|
||||
ComponentRef,
|
||||
DynamicComponentLoader,
|
||||
Injector,
|
||||
@ -7,12 +8,15 @@ import {
|
||||
ElementRef,
|
||||
EmbeddedViewRef,
|
||||
ChangeDetectorRef,
|
||||
provide
|
||||
provide,
|
||||
NgZone,
|
||||
NgZoneError
|
||||
} from 'angular2/core';
|
||||
import {DirectiveResolver, ViewResolver} from 'angular2/compiler';
|
||||
|
||||
import {Type, isPresent, isBlank} from 'angular2/src/facade/lang';
|
||||
import {PromiseWrapper} from 'angular2/src/facade/async';
|
||||
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||
import {Type, isPresent, isBlank, IS_DART} from 'angular2/src/facade/lang';
|
||||
import {PromiseWrapper, ObservableWrapper, PromiseCompleter} from 'angular2/src/facade/async';
|
||||
import {ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {el} from './utils';
|
||||
@ -24,6 +28,9 @@ import {DebugNode, DebugElement, getDebugNode} from 'angular2/src/core/debug/deb
|
||||
|
||||
import {tick} from './fake_async';
|
||||
|
||||
export var ComponentFixtureAutoDetect = new OpaqueToken("ComponentFixtureAutoDetect");
|
||||
export var ComponentFixtureNoNgZone = new OpaqueToken("ComponentFixtureNoNgZone");
|
||||
|
||||
/**
|
||||
* Fixture for debugging and testing a component.
|
||||
*/
|
||||
@ -58,31 +65,136 @@ export class ComponentFixture {
|
||||
*/
|
||||
changeDetectorRef: ChangeDetectorRef;
|
||||
|
||||
constructor(componentRef: ComponentRef) {
|
||||
/**
|
||||
* The NgZone in which this component was instantiated.
|
||||
*/
|
||||
ngZone: NgZone;
|
||||
|
||||
private _autoDetect: boolean;
|
||||
|
||||
private _isStable: boolean = true;
|
||||
private _completer: PromiseCompleter<any> = null;
|
||||
private _onUnstableSubscription = null;
|
||||
private _onStableSubscription = null;
|
||||
private _onMicrotaskEmptySubscription = null;
|
||||
private _onErrorSubscription = null;
|
||||
|
||||
constructor(componentRef: ComponentRef, ngZone: NgZone, autoDetect: boolean) {
|
||||
this.changeDetectorRef = componentRef.changeDetectorRef;
|
||||
this.elementRef = componentRef.location;
|
||||
this.debugElement = <DebugElement>getDebugNode(this.elementRef.nativeElement);
|
||||
this.componentInstance = componentRef.instance;
|
||||
this.nativeElement = this.elementRef.nativeElement;
|
||||
this.componentRef = componentRef;
|
||||
this.ngZone = ngZone;
|
||||
this._autoDetect = autoDetect;
|
||||
|
||||
if (ngZone != null) {
|
||||
this._onUnstableSubscription =
|
||||
ObservableWrapper.subscribe(ngZone.onUnstable, (_) => { this._isStable = false; });
|
||||
this._onMicrotaskEmptySubscription =
|
||||
ObservableWrapper.subscribe(ngZone.onMicrotaskEmpty, (_) => {
|
||||
if (this._autoDetect) {
|
||||
// Do a change detection run with checkNoChanges set to true to check
|
||||
// there are no changes on the second run.
|
||||
this.detectChanges(true);
|
||||
}
|
||||
});
|
||||
this._onStableSubscription = ObservableWrapper.subscribe(ngZone.onStable, (_) => {
|
||||
this._isStable = true;
|
||||
if (this._completer != null) {
|
||||
this._completer.resolve(true);
|
||||
this._completer = null;
|
||||
}
|
||||
});
|
||||
|
||||
this._onErrorSubscription = ObservableWrapper.subscribe(
|
||||
ngZone.onError, (error: NgZoneError) => { throw error.error; });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a change detection cycle for the component.
|
||||
*/
|
||||
detectChanges(checkNoChanges: boolean = true): void {
|
||||
private _tick(checkNoChanges: boolean) {
|
||||
this.changeDetectorRef.detectChanges();
|
||||
if (checkNoChanges) {
|
||||
this.checkNoChanges();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a change detection cycle for the component.
|
||||
*/
|
||||
detectChanges(checkNoChanges: boolean = true): void {
|
||||
if (this.ngZone != null) {
|
||||
// Run the change detection inside the NgZone so that any async tasks as part of the change
|
||||
// detection are captured by the zone and can be waited for in isStable.
|
||||
this.ngZone.run(() => { this._tick(checkNoChanges); });
|
||||
} else {
|
||||
// Running without zone. Just do the change detection.
|
||||
this._tick(checkNoChanges);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a change detection run to make sure there were no changes.
|
||||
*/
|
||||
checkNoChanges(): void { this.changeDetectorRef.checkNoChanges(); }
|
||||
|
||||
/**
|
||||
* Set whether the fixture should autodetect changes.
|
||||
*
|
||||
* Also runs detectChanges once so that any existing change is detected.
|
||||
*/
|
||||
autoDetectChanges(autoDetect: boolean = true) {
|
||||
if (this.ngZone == null) {
|
||||
throw new BaseException('Cannot call autoDetectChanges when ComponentFixtureNoNgZone is set');
|
||||
}
|
||||
this._autoDetect = autoDetect;
|
||||
this.detectChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the fixture is currently stable or has async tasks that have not been completed
|
||||
* yet.
|
||||
*/
|
||||
isStable(): boolean { return this._isStable; }
|
||||
|
||||
/**
|
||||
* Get a promise that resolves when the fixture is stable.
|
||||
*
|
||||
* This can be used to resume testing after events have triggered asynchronous activity or
|
||||
* asynchronous change detection.
|
||||
*/
|
||||
whenStable(): Promise<any> {
|
||||
if (this._isStable) {
|
||||
return PromiseWrapper.resolve(false);
|
||||
} else {
|
||||
this._completer = new PromiseCompleter<any>();
|
||||
return this._completer.promise;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger component destruction.
|
||||
*/
|
||||
destroy(): void { this.componentRef.destroy(); }
|
||||
destroy(): void {
|
||||
this.componentRef.destroy();
|
||||
if (this._onUnstableSubscription != null) {
|
||||
ObservableWrapper.dispose(this._onUnstableSubscription);
|
||||
this._onUnstableSubscription = null;
|
||||
}
|
||||
if (this._onStableSubscription != null) {
|
||||
ObservableWrapper.dispose(this._onStableSubscription);
|
||||
this._onStableSubscription = null;
|
||||
}
|
||||
if (this._onMicrotaskEmptySubscription != null) {
|
||||
ObservableWrapper.dispose(this._onMicrotaskEmptySubscription);
|
||||
this._onMicrotaskEmptySubscription = null;
|
||||
}
|
||||
if (this._onErrorSubscription != null) {
|
||||
ObservableWrapper.dispose(this._onErrorSubscription);
|
||||
this._onErrorSubscription = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var _nextRootElementId = 0;
|
||||
@ -108,7 +220,7 @@ export class TestComponentBuilder {
|
||||
|
||||
/** @internal */
|
||||
_clone(): TestComponentBuilder {
|
||||
var clone = new TestComponentBuilder(this._injector);
|
||||
let clone = new TestComponentBuilder(this._injector);
|
||||
clone._viewOverrides = MapWrapper.clone(this._viewOverrides);
|
||||
clone._directiveOverrides = MapWrapper.clone(this._directiveOverrides);
|
||||
clone._templateOverrides = MapWrapper.clone(this._templateOverrides);
|
||||
@ -127,7 +239,7 @@ export class TestComponentBuilder {
|
||||
* @return {TestComponentBuilder}
|
||||
*/
|
||||
overrideTemplate(componentType: Type, template: string): TestComponentBuilder {
|
||||
var clone = this._clone();
|
||||
let clone = this._clone();
|
||||
clone._templateOverrides.set(componentType, template);
|
||||
return clone;
|
||||
}
|
||||
@ -141,7 +253,7 @@ export class TestComponentBuilder {
|
||||
* @return {TestComponentBuilder}
|
||||
*/
|
||||
overrideView(componentType: Type, view: ViewMetadata): TestComponentBuilder {
|
||||
var clone = this._clone();
|
||||
let clone = this._clone();
|
||||
clone._viewOverrides.set(componentType, view);
|
||||
return clone;
|
||||
}
|
||||
@ -156,8 +268,8 @@ export class TestComponentBuilder {
|
||||
* @return {TestComponentBuilder}
|
||||
*/
|
||||
overrideDirective(componentType: Type, from: Type, to: Type): TestComponentBuilder {
|
||||
var clone = this._clone();
|
||||
var overridesForComponent = clone._directiveOverrides.get(componentType);
|
||||
let clone = this._clone();
|
||||
let overridesForComponent = clone._directiveOverrides.get(componentType);
|
||||
if (!isPresent(overridesForComponent)) {
|
||||
clone._directiveOverrides.set(componentType, new Map<Type, Type>());
|
||||
overridesForComponent = clone._directiveOverrides.get(componentType);
|
||||
@ -182,7 +294,7 @@ export class TestComponentBuilder {
|
||||
* @return {TestComponentBuilder}
|
||||
*/
|
||||
overrideProviders(type: Type, providers: any[]): TestComponentBuilder {
|
||||
var clone = this._clone();
|
||||
let clone = this._clone();
|
||||
clone._bindingsOverrides.set(type, providers);
|
||||
return clone;
|
||||
}
|
||||
@ -210,7 +322,7 @@ export class TestComponentBuilder {
|
||||
* @return {TestComponentBuilder}
|
||||
*/
|
||||
overrideViewProviders(type: Type, providers: any[]): TestComponentBuilder {
|
||||
var clone = this._clone();
|
||||
let clone = this._clone();
|
||||
clone._viewBindingsOverrides.set(type, providers);
|
||||
return clone;
|
||||
}
|
||||
@ -228,40 +340,49 @@ export class TestComponentBuilder {
|
||||
* @return {Promise<ComponentFixture>}
|
||||
*/
|
||||
createAsync(rootComponentType: Type): Promise<ComponentFixture> {
|
||||
var mockDirectiveResolver = this._injector.get(DirectiveResolver);
|
||||
var mockViewResolver = this._injector.get(ViewResolver);
|
||||
this._viewOverrides.forEach((view, type) => mockViewResolver.setView(type, view));
|
||||
this._templateOverrides.forEach((template, type) =>
|
||||
mockViewResolver.setInlineTemplate(type, template));
|
||||
this._directiveOverrides.forEach((overrides, component) => {
|
||||
overrides.forEach(
|
||||
(to, from) => { mockViewResolver.overrideViewDirective(component, from, to); });
|
||||
});
|
||||
this._bindingsOverrides.forEach((bindings, type) =>
|
||||
mockDirectiveResolver.setBindingsOverride(type, bindings));
|
||||
this._viewBindingsOverrides.forEach(
|
||||
(bindings, type) => mockDirectiveResolver.setViewBindingsOverride(type, bindings));
|
||||
let noNgZone = IS_DART || this._injector.get(ComponentFixtureNoNgZone, false);
|
||||
let ngZone: NgZone = noNgZone ? null : this._injector.get(NgZone, null);
|
||||
let autoDetect: boolean = this._injector.get(ComponentFixtureAutoDetect, false);
|
||||
|
||||
var rootElId = `root${_nextRootElementId++}`;
|
||||
var rootEl = el(`<div id="${rootElId}"></div>`);
|
||||
var doc = this._injector.get(DOCUMENT);
|
||||
let initComponent = () => {
|
||||
let mockDirectiveResolver = this._injector.get(DirectiveResolver);
|
||||
let mockViewResolver = this._injector.get(ViewResolver);
|
||||
this._viewOverrides.forEach((view, type) => mockViewResolver.setView(type, view));
|
||||
this._templateOverrides.forEach((template, type) =>
|
||||
mockViewResolver.setInlineTemplate(type, template));
|
||||
this._directiveOverrides.forEach((overrides, component) => {
|
||||
overrides.forEach(
|
||||
(to, from) => { mockViewResolver.overrideViewDirective(component, from, to); });
|
||||
});
|
||||
this._bindingsOverrides.forEach(
|
||||
(bindings, type) => mockDirectiveResolver.setBindingsOverride(type, bindings));
|
||||
this._viewBindingsOverrides.forEach(
|
||||
(bindings, type) => mockDirectiveResolver.setViewBindingsOverride(type, bindings));
|
||||
|
||||
// TODO(juliemr): can/should this be optional?
|
||||
var oldRoots = DOM.querySelectorAll(doc, '[id^=root]');
|
||||
for (var i = 0; i < oldRoots.length; i++) {
|
||||
DOM.remove(oldRoots[i]);
|
||||
}
|
||||
DOM.appendChild(doc.body, rootEl);
|
||||
let rootElId = `root${_nextRootElementId++}`;
|
||||
let rootEl = el(`<div id="${rootElId}"></div>`);
|
||||
let doc = this._injector.get(DOCUMENT);
|
||||
|
||||
var promise: Promise<ComponentRef> =
|
||||
this._injector.get(DynamicComponentLoader)
|
||||
.loadAsRoot(rootComponentType, `#${rootElId}`, this._injector);
|
||||
return promise.then((componentRef) => { return new ComponentFixture(componentRef); });
|
||||
// TODO(juliemr): can/should this be optional?
|
||||
let oldRoots = DOM.querySelectorAll(doc, '[id^=root]');
|
||||
for (let i = 0; i < oldRoots.length; i++) {
|
||||
DOM.remove(oldRoots[i]);
|
||||
}
|
||||
DOM.appendChild(doc.body, rootEl);
|
||||
|
||||
let promise: Promise<ComponentRef> =
|
||||
this._injector.get(DynamicComponentLoader)
|
||||
.loadAsRoot(rootComponentType, `#${rootElId}`, this._injector);
|
||||
return promise.then(
|
||||
(componentRef) => { return new ComponentFixture(componentRef, ngZone, autoDetect); });
|
||||
};
|
||||
|
||||
return ngZone == null ? initComponent() : ngZone.run(initComponent);
|
||||
}
|
||||
|
||||
createFakeAsync(rootComponentType: Type): ComponentFixture {
|
||||
var result;
|
||||
var error;
|
||||
let result;
|
||||
let error;
|
||||
PromiseWrapper.then(this.createAsync(rootComponentType), (_result) => { result = _result; },
|
||||
(_error) => { error = _error; });
|
||||
tick();
|
||||
|
@ -157,7 +157,7 @@ export class InjectSetupWrapper {
|
||||
inject(tokens: any[], fn: Function): Function {
|
||||
return () => {
|
||||
this._addProviders();
|
||||
return inject(tokens, fn)();
|
||||
return inject_impl(tokens, fn)();
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,7 +165,7 @@ export class InjectSetupWrapper {
|
||||
injectAsync(tokens: any[], fn: Function): Function {
|
||||
return () => {
|
||||
this._addProviders();
|
||||
return injectAsync(tokens, fn)();
|
||||
return injectAsync_impl(tokens, fn)();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -197,3 +197,8 @@ export function withProviders(providers: () => any) {
|
||||
export function injectAsync(tokens: any[], fn: Function): Function {
|
||||
return async(inject(tokens, fn));
|
||||
}
|
||||
|
||||
// This is to ensure inject(Async) within InjectSetupWrapper doesn't call itself
|
||||
// when transpiled to Dart.
|
||||
var inject_impl = inject;
|
||||
var injectAsync_impl = injectAsync;
|
||||
|
Reference in New Issue
Block a user