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.
This commit is contained in:
Yi Qi 2017-09-08 11:50:13 -07:00 committed by Matias Niemelä
parent 831613aab5
commit 97cc6caa33
6 changed files with 229 additions and 6 deletions

View File

@ -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);
}

View File

@ -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);
});
});
});
}

View File

@ -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 {

View File

@ -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);

View File

@ -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('<input type="date" name="myDate">'));
expect(projectableNodes[1]).toEqual(nodes('<span>span content</span>'));
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<any>; // testbed
let wrapCallback = undefined as any;
content = `
<h1> new component </h1>
<div> a great component </div>
<comp></comp>
`;
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);
});
});
});
}
};

View File

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