Skip to content

Commit

Permalink
fix(upgrade): add testability hook to downgraded component
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
qiyigg authored and matsko committed Sep 12, 2017
1 parent 831613a commit 97cc6ca
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 6 deletions.
54 changes: 54 additions & 0 deletions packages/core/src/testability/testability.ts
Expand Up @@ -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) {
Expand All @@ -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;
}
Expand All @@ -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 [];
Expand All @@ -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);
}
Expand Down
42 changes: 41 additions & 1 deletion packages/core/test/testability/testability_spec.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
});
});
});
}
1 change: 1 addition & 0 deletions packages/upgrade/src/common/angular1.ts
Expand Up @@ -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 {
Expand Down
14 changes: 13 additions & 1 deletion packages/upgrade/src/common/downgrade_component_adapter.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
Expand Down
122 changes: 118 additions & 4 deletions packages/upgrade/test/common/downgrade_component_adapter_spec.ts
Expand Up @@ -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', () => {
Expand All @@ -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])
Expand Down Expand Up @@ -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);
});
});

});
}
};
2 changes: 2 additions & 0 deletions tools/public_api_guard/core/index.d.ts
Expand Up @@ -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 */
Expand Down

0 comments on commit 97cc6ca

Please sign in to comment.