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 */