diff --git a/packages/core/src/testability/testability.ts b/packages/core/src/testability/testability.ts index d04abc69c3..f39e0bb823 100644 --- a/packages/core/src/testability/testability.ts +++ b/packages/core/src/testability/testability.ts @@ -67,12 +67,18 @@ export class Testability implements PublicTestability { }); } + /** + * Increases the number of pending request + */ increasePendingRequestCount(): number { this._pendingCount += 1; this._didWork = true; return this._pendingCount; } + /** + * Decreases the number of pending request + */ decreasePendingRequestCount(): number { this._pendingCount -= 1; if (this._pendingCount < 0) { @@ -82,6 +88,9 @@ export class Testability implements PublicTestability { return this._pendingCount; } + /** + * Whether an associated application is stable + */ isStable(): boolean { return this._isZoneStable && this._pendingCount == 0 && !this._ngZone.hasPendingMacrotasks; } @@ -102,13 +111,26 @@ export class Testability implements PublicTestability { } } + /** + * Run callback when the application is stable + * @param callback function to be called after the application is stable + */ whenStable(callback: Function): void { this._callbacks.push(callback); this._runCallbacksIfReady(); } + /** + * Get the number of pending requests + */ getPendingRequestCount(): number { return this._pendingCount; } + /** + * Find providers by name + * @param using The root element to search from + * @param provider The name of binding variable + * @param exactMatch Whether using exactMatch + */ findProviders(using: any, provider: string, exactMatch: boolean): any[] { // TODO(juliemr): implement. return []; @@ -126,16 +148,48 @@ export class TestabilityRegistry { constructor() { _testabilityGetter.addToWindow(this); } + /** + * Registers an application with a testability hook so that it can be tracked + * @param token token of application, root element + * @param testability Testability hook + */ registerApplication(token: any, testability: Testability) { this._applications.set(token, testability); } + /** + * Unregisters an application. + * @param token token of application, root element + */ + unregisterApplication(token: any) { this._applications.delete(token); } + + /** + * Unregisters all applications + */ + unregisterAllApplications() { this._applications.clear(); } + + /** + * Get a testability hook associated with the application + * @param elem root element + */ getTestability(elem: any): Testability|null { return this._applications.get(elem) || null; } + /** + * Get all registered testabilities + */ getAllTestabilities(): Testability[] { return Array.from(this._applications.values()); } + /** + * Get all registered applications(root elements) + */ getAllRootElements(): any[] { return Array.from(this._applications.keys()); } + /** + * Find testability of a node in the Tree + * @param elem node + * @param findInAncestors whether finding testability in ancestors if testability was not found in + * current node + */ findTestabilityInTree(elem: Node, findInAncestors: boolean = true): Testability|null { return _testabilityGetter.findTestabilityInTree(this, elem, findInAncestors); } diff --git a/packages/core/test/testability/testability_spec.ts b/packages/core/test/testability/testability_spec.ts index 95af50ffaf..d65b6fb360 100644 --- a/packages/core/test/testability/testability_spec.ts +++ b/packages/core/test/testability/testability_spec.ts @@ -8,7 +8,7 @@ import {EventEmitter} from '@angular/core'; import {Injectable} from '@angular/core/src/di'; -import {Testability} from '@angular/core/src/testability/testability'; +import {Testability, TestabilityRegistry} from '@angular/core/src/testability/testability'; import {NgZone} from '@angular/core/src/zone/ng_zone'; import {AsyncTestCompleter, SpyObject, beforeEach, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal'; @@ -280,4 +280,44 @@ export function main() { })); }); }); + + describe('TestabilityRegistry', () => { + let testability1: Testability; + let testability2: Testability; + let resgitry: TestabilityRegistry; + let ngZone: MockNgZone; + + beforeEach(() => { + ngZone = new MockNgZone(); + testability1 = new Testability(ngZone); + testability2 = new Testability(ngZone); + resgitry = new TestabilityRegistry(); + }); + describe('unregister testability', () => { + it('should remove the testability when unregistering an existing testability', () => { + resgitry.registerApplication('testability1', testability1); + resgitry.registerApplication('testability2', testability2); + resgitry.unregisterApplication('testability2'); + expect(resgitry.getAllTestabilities().length).toEqual(1); + expect(resgitry.getTestability('testability1')).toEqual(testability1); + }); + + it('should remain the same when unregistering a non-existing testability', () => { + expect(resgitry.getAllTestabilities().length).toEqual(0); + resgitry.registerApplication('testability1', testability1); + resgitry.registerApplication('testability2', testability2); + resgitry.unregisterApplication('testability3'); + expect(resgitry.getAllTestabilities().length).toEqual(2); + expect(resgitry.getTestability('testability1')).toEqual(testability1); + expect(resgitry.getTestability('testability2')).toEqual(testability2); + }); + + it('should remove all the testability when unregistering all testabilities', () => { + resgitry.registerApplication('testability1', testability1); + resgitry.registerApplication('testability2', testability2); + resgitry.unregisterAllApplications(); + expect(resgitry.getAllTestabilities().length).toEqual(0); + }); + }); + }); } diff --git a/packages/upgrade/src/common/angular1.ts b/packages/upgrade/src/common/angular1.ts index 76a7864209..329573ddb9 100644 --- a/packages/upgrade/src/common/angular1.ts +++ b/packages/upgrade/src/common/angular1.ts @@ -127,6 +127,7 @@ export type IAugmentedJQuery = Node[] & { controller?: (name: string) => any; isolateScope?: () => IScope; injector?: () => IInjectorService; + remove?: () => void; }; export interface IProvider { $get: IInjectable; } export interface IProvideService { diff --git a/packages/upgrade/src/common/downgrade_component_adapter.ts b/packages/upgrade/src/common/downgrade_component_adapter.ts index 93f7cae8c8..13bb1ed9a4 100644 --- a/packages/upgrade/src/common/downgrade_component_adapter.ts +++ b/packages/upgrade/src/common/downgrade_component_adapter.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationRef, ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, Type} from '@angular/core'; +import {ApplicationRef, ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, Testability, TestabilityRegistry, Type} from '@angular/core'; import * as angular from './angular1'; import {PropertyBinding} from './component_info'; @@ -64,6 +64,16 @@ export class DowngradeComponentAdapter { this.changeDetector = this.componentRef.changeDetectorRef; this.component = this.componentRef.instance; + // testability hook is commonly added during component bootstrap in + // packages/core/src/application_ref.bootstrap() + // in downgraded application, component creation will take place here as well as adding the + // testability hook. + const testability = this.componentRef.injector.get(Testability, null); + if (testability) { + this.componentRef.injector.get(TestabilityRegistry) + .registerApplication(this.componentRef.location.nativeElement, testability); + } + hookupNgModel(this.ngModel, this.component); } @@ -195,6 +205,8 @@ export class DowngradeComponentAdapter { registerCleanup(needsNgZone: boolean) { this.element.on !('$destroy', () => { this.componentScope.$destroy(); + this.componentRef.injector.get(TestabilityRegistry) + .unregisterApplication(this.componentRef.location.nativeElement); this.componentRef.destroy(); if (needsNgZone) { this.appRef.detachView(this.componentRef.hostView); diff --git a/packages/upgrade/test/common/downgrade_component_adapter_spec.ts b/packages/upgrade/test/common/downgrade_component_adapter_spec.ts index 730c2495cd..87443a212e 100644 --- a/packages/upgrade/test/common/downgrade_component_adapter_spec.ts +++ b/packages/upgrade/test/common/downgrade_component_adapter_spec.ts @@ -5,10 +5,12 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import {ApplicationRef, Compiler, Component, ComponentFactory, ComponentRef, Injector, NgModule, Testability, TestabilityRegistry} from '@angular/core'; +import {TestBed, getTestBed, inject} from '@angular/core/testing'; import * as angular from '@angular/upgrade/src/common/angular1'; -import {groupNodesBySelector} from '@angular/upgrade/src/common/downgrade_component_adapter'; -import {nodes} from './test_helpers'; +import {DowngradeComponentAdapter, groupNodesBySelector} from '@angular/upgrade/src/common/downgrade_component_adapter'; +import {nodes} from './test_helpers'; export function main() { describe('DowngradeComponentAdapter', () => { @@ -23,7 +25,6 @@ export function main() { const selectors = ['input[type=date]', 'span', '.x']; const projectableNodes = groupNodesBySelector(selectors, contentNodes); - expect(projectableNodes[0]).toEqual(nodes('')); expect(projectableNodes[1]).toEqual(nodes('span content')); expect(projectableNodes[2]) @@ -75,5 +76,118 @@ export function main() { expect(noMatchSelectorNodes).toEqual([[]]); }); }); + + describe('testability', () => { + + let adapter: DowngradeComponentAdapter; + let content: string; + let compiler: Compiler; + let element: angular.IAugmentedJQuery; + + class mockScope implements angular.IScope { + $new() { return this; }; + $watch(exp: angular.Ng1Expression, fn?: (a1?: any, a2?: any) => void) { + return () => {}; + }; + $on(event: string, fn?: (event?: any, ...args: any[]) => void) { + return () => {}; + }; + $destroy() { + return () => {}; + }; + $apply(exp?: angular.Ng1Expression) { + return () => {}; + }; + $digest() { + return () => {}; + }; + $evalAsync(exp: angular.Ng1Expression, locals?: any) { + return () => {}; + }; + $$childTail: angular.IScope; + $$childHead: angular.IScope; + $$nextSibling: angular.IScope; + [key: string]: any; + $id = 'mockScope'; + $parent: angular.IScope; + $root: angular.IScope; + } + + function getAdaptor(): DowngradeComponentAdapter { + let attrs = undefined as any; + let scope: angular.IScope; // mock + let ngModel = undefined as any; + let parentInjector: Injector; // testbed + let $injector = undefined as any; + let $compile = undefined as any; + let $parse = undefined as any; + let componentFactory: ComponentFactory; // testbed + let wrapCallback = undefined as any; + + content = ` +

new component

+
a great component
+ + `; + element = angular.element(content); + scope = new mockScope(); + + @Component({ + selector: 'comp', + template: '', + }) + class NewComponent { + } + + @NgModule({ + providers: [{provide: 'hello', useValue: 'component'}], + declarations: [NewComponent], + entryComponents: [NewComponent], + }) + class NewModule { + } + + const modFactory = compiler.compileModuleSync(NewModule); + const module = modFactory.create(TestBed); + componentFactory = module.componentFactoryResolver.resolveComponentFactory(NewComponent) !; + parentInjector = TestBed; + + return new DowngradeComponentAdapter( + element, attrs, scope, ngModel, parentInjector, $injector, $compile, $parse, + componentFactory, wrapCallback); + }; + + beforeEach((inject([Compiler], (inject_compiler: Compiler) => { + compiler = inject_compiler; + adapter = getAdaptor(); + }))); + + afterEach(() => { + let registry = TestBed.get(TestabilityRegistry); + registry.unregisterAllApplications(); + }); + + it('should add testabilities hook when creating components', () => { + + let registry = TestBed.get(TestabilityRegistry); + adapter.createComponent([]); + expect(registry.getAllTestabilities().length).toEqual(1); + + adapter = getAdaptor(); // get a new adaptor to creat a new component + adapter.createComponent([]); + expect(registry.getAllTestabilities().length).toEqual(2); + }); + + it('should remove the testability hook when destroy a component', () => { + const registry = TestBed.get(TestabilityRegistry); + expect(registry.getAllTestabilities().length).toEqual(0); + adapter.createComponent([]); + expect(registry.getAllTestabilities().length).toEqual(1); + adapter.registerCleanup(true); + element.remove !(); + expect(registry.getAllTestabilities().length).toEqual(0); + }); + }); + }); -} +}; diff --git a/tools/public_api_guard/core/index.d.ts b/tools/public_api_guard/core/index.d.ts index e4fc286625..ae7a2c3c8e 100644 --- a/tools/public_api_guard/core/index.d.ts +++ b/tools/public_api_guard/core/index.d.ts @@ -977,6 +977,8 @@ export declare class TestabilityRegistry { getAllTestabilities(): Testability[]; getTestability(elem: any): Testability | null; registerApplication(token: any, testability: Testability): void; + unregisterAllApplications(): void; + unregisterApplication(token: any): void; } /** @stable */