diff --git a/modules/angular2/platform/testing/browser_static.ts b/modules/angular2/platform/testing/browser_static.ts index 86f5984372..95fe96f8f2 100644 --- a/modules/angular2/platform/testing/browser_static.ts +++ b/modules/angular2/platform/testing/browser_static.ts @@ -20,13 +20,17 @@ import {MockNgZone} from 'angular2/src/mock/ng_zone_mock'; import {XHRImpl} from "angular2/src/platform/browser/xhr_impl"; import {XHR} from 'angular2/compiler'; -import {TestComponentBuilder} from 'angular2/src/testing/test_component_builder'; +import { + TestComponentBuilder, + ComponentFixtureAutoDetect, + ComponentFixtureNoNgZone +} from 'angular2/src/testing/test_component_builder'; import {BrowserDetection} from 'angular2/src/testing/utils'; import {ELEMENT_PROBE_PROVIDERS} from 'angular2/platform/common_dom'; -import {CONST_EXPR} from 'angular2/src/facade/lang'; +import {CONST_EXPR, IS_DART} from 'angular2/src/facade/lang'; import {Log} from 'angular2/src/testing/utils'; @@ -35,6 +39,10 @@ function initBrowserTests() { BrowserDetection.setup(); } +function createNgZone(): NgZone { + return IS_DART ? new MockNgZone() : new NgZone({enableLongStackTrace: true}); +} + /** * Default platform providers for testing without a compiler. */ @@ -52,7 +60,7 @@ export const ADDITIONAL_TEST_BROWSER_PROVIDERS: Array = 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 = 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 { + if (this._isStable) { + return PromiseWrapper.resolve(false); + } else { + this._completer = new PromiseCompleter(); + 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()); 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} */ createAsync(rootComponentType: Type): Promise { - 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(`
`); - 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(`
`); + let doc = this._injector.get(DOCUMENT); - var promise: Promise = - 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 = + 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(); diff --git a/modules/angular2/src/testing/test_injector.ts b/modules/angular2/src/testing/test_injector.ts index dfa0723401..fac29151a5 100644 --- a/modules/angular2/src/testing/test_injector.ts +++ b/modules/angular2/src/testing/test_injector.ts @@ -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; diff --git a/modules/angular2/test/core/linker/dynamic_component_loader_spec.ts b/modules/angular2/test/core/linker/dynamic_component_loader_spec.ts index b37b97abcd..3dfc54ea9f 100644 --- a/modules/angular2/test/core/linker/dynamic_component_loader_spec.ts +++ b/modules/angular2/test/core/linker/dynamic_component_loader_spec.ts @@ -146,7 +146,7 @@ export function main() { DOM.appendChild(doc.body, rootEl); loader.loadAsRoot(ChildComp, null, injector) .then((componentRef) => { - var el = new ComponentFixture(componentRef); + var el = new ComponentFixture(componentRef, null, false); expect(rootEl.parentNode).toBe(doc.body); diff --git a/modules/angular2/test/testing/test_component_builder_spec.ts b/modules/angular2/test/testing/test_component_builder_spec.ts index e6c1b2bb3d..fd36eed25a 100644 --- a/modules/angular2/test/testing/test_component_builder_spec.ts +++ b/modules/angular2/test/testing/test_component_builder_spec.ts @@ -9,15 +9,20 @@ import { iit, inject, beforeEachProviders, + withProviders, it, xit, - TestComponentBuilder + TestComponentBuilder, + ComponentFixtureAutoDetect, + ComponentFixtureNoNgZone } from 'angular2/testing_internal'; import {CONST_EXPR} from 'angular2/src/facade/lang'; import {Injectable, provide} from 'angular2/core'; import {NgIf} from 'angular2/common'; -import {Directive, Component, ViewMetadata} from 'angular2/src/core/metadata'; +import {Directive, Component, ViewMetadata, Input} from 'angular2/src/core/metadata'; +import {IS_DART} from 'angular2/src/facade/lang'; +import {PromiseWrapper} from 'angular2/src/facade/promise'; @Component( {selector: 'child-comp', template: `Original {{childBinding}}`, directives: []}) @@ -72,7 +77,42 @@ class ChildWithChildComp { class MockChildChildComp { } +@Component({selector: 'autodetect-comp', template: `{{text}}`}) +class AutoDetectComp { + text: string = '1'; + click() { this.text += '1'; } +} + +@Component({selector: 'async-comp', template: `{{text}}`}) +class AsyncComp { + text: string = '1'; + + click() { + PromiseWrapper.resolve(null).then((_) => { this.text += '1'; }); + } +} + +@Component({selector: 'async-child-comp', template: '{{localText}}'}) +class AsyncChildComp { + localText: string = ''; + + @Input() + set text(value: string) { + PromiseWrapper.resolve(null).then((_) => { this.localText = value; }); + } +} + +@Component({ + selector: 'async-change-comp', + template: ``, + directives: [AsyncChildComp] +}) +class AsyncChangeComp { + text: string = '1'; + + click() { this.text += '1'; } +} class FancyService { value: string = 'real value'; @@ -244,5 +284,184 @@ export function main() { async.done(); }); })); + + if (!IS_DART) { + describe('ComponentFixture', () => { + it('should auto detect changes if autoDetectChanges is called', + inject([TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async) => { + + tcb.createAsync(AutoDetectComp) + .then((componentFixture) => { + expect(componentFixture.ngZone).not.toBeNull(); + componentFixture.autoDetectChanges(); + expect(componentFixture.nativeElement).toHaveText('1'); + + let element = componentFixture.debugElement.children[0]; + dispatchEvent(element.nativeElement, 'click'); + + expect(componentFixture.isStable()).toBe(true); + expect(componentFixture.nativeElement).toHaveText('11'); + async.done(); + }); + })); + + it('should auto detect changes if ComponentFixtureAutoDetect is provided as true', + withProviders(() => [provide(ComponentFixtureAutoDetect, {useValue: true})]) + .inject([TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async) => { + + tcb.createAsync(AutoDetectComp) + .then((componentFixture) => { + expect(componentFixture.nativeElement).toHaveText('1'); + + let element = componentFixture.debugElement.children[0]; + dispatchEvent(element.nativeElement, 'click'); + + expect(componentFixture.nativeElement).toHaveText('11'); + async.done(); + }); + })); + + it('should signal through whenStable when the fixture is stable (autoDetectChanges)', + inject([TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async) => { + + tcb.createAsync(AsyncComp).then((componentFixture) => { + componentFixture.autoDetectChanges(); + expect(componentFixture.nativeElement).toHaveText('1'); + + let element = componentFixture.debugElement.children[0]; + dispatchEvent(element.nativeElement, 'click'); + expect(componentFixture.nativeElement).toHaveText('1'); + + // Component is updated asynchronously. Wait for the fixture to become stable + // before checking for new value. + expect(componentFixture.isStable()).toBe(false); + componentFixture.whenStable().then((waited) => { + expect(waited).toBe(true); + expect(componentFixture.nativeElement).toHaveText('11'); + async.done(); + }); + }); + })); + + it('should signal through isStable when the fixture is stable (no autoDetectChanges)', + inject([TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async) => { + + tcb.createAsync(AsyncComp).then((componentFixture) => { + componentFixture.detectChanges(); + expect(componentFixture.nativeElement).toHaveText('1'); + + let element = componentFixture.debugElement.children[0]; + dispatchEvent(element.nativeElement, 'click'); + expect(componentFixture.nativeElement).toHaveText('1'); + + // Component is updated asynchronously. Wait for the fixture to become stable + // before checking. + componentFixture.whenStable().then((waited) => { + expect(waited).toBe(true); + componentFixture.detectChanges(); + expect(componentFixture.nativeElement).toHaveText('11'); + async.done(); + }); + }); + })); + + it('should stabilize after async task in change detection (autoDetectChanges)', + inject([TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async) => { + + tcb.createAsync(AsyncChangeComp) + .then((componentFixture) => { + componentFixture.autoDetectChanges(); + componentFixture.whenStable().then((_) => { + expect(componentFixture.nativeElement).toHaveText('1'); + + let element = componentFixture.debugElement.children[0]; + dispatchEvent(element.nativeElement, 'click'); + + componentFixture.whenStable().then((_) => { + expect(componentFixture.nativeElement).toHaveText('11'); + async.done(); + }); + }); + }); + })); + + it('should stabilize after async task in change detection(no autoDetectChanges)', + inject([TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async) => { + + tcb.createAsync(AsyncChangeComp) + .then((componentFixture) => { + componentFixture.detectChanges(); + componentFixture.whenStable().then((_) => { + // Run detectChanges again so that stabilized value is reflected in the + // DOM. + componentFixture.detectChanges(); + expect(componentFixture.nativeElement).toHaveText('1'); + + let element = componentFixture.debugElement.children[0]; + dispatchEvent(element.nativeElement, 'click'); + componentFixture.detectChanges(); + + componentFixture.whenStable().then((_) => { + // Run detectChanges again so that stabilized value is reflected in + // the DOM. + componentFixture.detectChanges(); + expect(componentFixture.nativeElement).toHaveText('11'); + async.done(); + }); + }); + }); + })); + + describe('No NgZone', () => { + beforeEachProviders(() => [provide(ComponentFixtureNoNgZone, {useValue: true})]); + + it('calling autoDetectChanges raises an error', () => { + inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, + async) => { + tcb.createAsync(ChildComp).then((componentFixture) => { + expect(() => { + componentFixture.autoDetectChanges(); + }).toThrow('Cannot call autoDetectChanges when ComponentFixtureNoNgZone is set!!'); + async.done(); + }); + }); + }); + + it('should instantiate a component with valid DOM', + inject([TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async) => { + + tcb.createAsync(ChildComp).then((componentFixture) => { + expect(componentFixture.ngZone).toBeNull(); + componentFixture.detectChanges(); + expect(componentFixture.nativeElement).toHaveText('Original Child'); + async.done(); + }); + })); + + it('should allow changing members of the component', + inject([TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async) => { + + tcb.createAsync(MyIfComp).then((componentFixture) => { + componentFixture.detectChanges(); + expect(componentFixture.nativeElement).toHaveText('MyIf()'); + + componentFixture.componentInstance.showMore = true; + componentFixture.detectChanges(); + expect(componentFixture.nativeElement).toHaveText('MyIf(More)'); + + async.done(); + }); + })); + }); + }); + } }); } diff --git a/modules/angular2/test/web_workers/shared/message_bus_spec.ts b/modules/angular2/test/web_workers/shared/message_bus_spec.ts index 71698ce22c..42a4c2aa88 100644 --- a/modules/angular2/test/web_workers/shared/message_bus_spec.ts +++ b/modules/angular2/test/web_workers/shared/message_bus_spec.ts @@ -1,5 +1,6 @@ import { AsyncTestCompleter, + withProviders, inject, describe, it, @@ -8,6 +9,7 @@ import { beforeEachProviders, SpyObject } from 'angular2/testing_internal'; +import {provide} from 'angular2/src/core/di'; import {ObservableWrapper, TimerWrapper} from 'angular2/src/facade/async'; import {MessageBus} from 'angular2/src/web_workers/shared/message_bus'; import {createConnectedMessageBus} from './message_bus_util'; @@ -111,30 +113,34 @@ export function main() { // TODO(mlaval): timeout is fragile, test to be rewritten function flushMessages(fn: () => void) { TimerWrapper.setTimeout(fn, 50); } - beforeEach(() => { bus = createConnectedMessageBus(); }); - it("should buffer messages and wait for the zone to exit before sending", - inject([AsyncTestCompleter, NgZone], (async, zone: MockNgZone) => { - setup(true, zone); + withProviders(() => [provide(NgZone, {useClass: MockNgZone})]) + .inject([AsyncTestCompleter, NgZone], + (async, zone: MockNgZone) => { + bus = createConnectedMessageBus(); + setup(true, zone); - var wasCalled = false; - ObservableWrapper.subscribe(bus.from(CHANNEL), (message) => { wasCalled = true; }); - ObservableWrapper.callEmit(bus.to(CHANNEL), "hi"); + var wasCalled = false; + ObservableWrapper.subscribe(bus.from(CHANNEL), + (message) => { wasCalled = true; }); + ObservableWrapper.callEmit(bus.to(CHANNEL), "hi"); - flushMessages(() => { - expect(wasCalled).toBeFalsy(); + flushMessages(() => { + expect(wasCalled).toBeFalsy(); - zone.simulateZoneExit(); - flushMessages(() => { - expect(wasCalled).toBeTruthy(); - async.done(); - }); - }); - }), 500); + zone.simulateZoneExit(); + flushMessages(() => { + expect(wasCalled).toBeTruthy(); + async.done(); + }); + }); + }), + 500); it("should send messages immediatly when run outside the zone", inject([AsyncTestCompleter, NgZone], (async, zone: MockNgZone) => { + bus = createConnectedMessageBus(); setup(false, zone); var wasCalled = false; diff --git a/modules/angular2/testing.ts b/modules/angular2/testing.ts index 973f04afe6..0d26488520 100644 --- a/modules/angular2/testing.ts +++ b/modules/angular2/testing.ts @@ -7,7 +7,12 @@ * */ export * from './src/testing/testing'; -export {ComponentFixture, TestComponentBuilder} from './src/testing/test_component_builder'; +export { + ComponentFixture, + TestComponentBuilder, + ComponentFixtureAutoDetect, + ComponentFixtureNoNgZone +} from './src/testing/test_component_builder'; export * from './src/testing/test_injector'; export * from './src/testing/fake_async';