From 97cc6caa33912b4534773563bc2ff19bebbf3644 Mon Sep 17 00:00:00 2001 From: Yi Qi Date: Fri, 8 Sep 2017 11:50:13 -0700 Subject: [PATCH] fix(upgrade): add testability hook to downgraded component Add testability hook to downgraded component so that protractor can wait for asynchronous call to complete. Add unregisterApplication() and unregisterAllApplications() to testability registry for cleaning up testability and unit test. --- packages/core/src/testability/testability.ts | 54 ++++++++ .../core/test/testability/testability_spec.ts | 42 +++++- packages/upgrade/src/common/angular1.ts | 1 + .../src/common/downgrade_component_adapter.ts | 14 +- .../downgrade_component_adapter_spec.ts | 122 +++++++++++++++++- tools/public_api_guard/core/index.d.ts | 2 + 6 files changed, 229 insertions(+), 6 deletions(-) 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 */