refactor(upgrade): extract promise-related utilities to separate file and add tests (#31840)

PR Close #31840
This commit is contained in:
George Kalpakas 2019-07-25 14:01:32 +03:00 committed by Alex Rickabaugh
parent 82b97280f3
commit b3b5c66414
3 changed files with 141 additions and 28 deletions

View File

@ -11,13 +11,10 @@ import {ComponentFactory, ComponentFactoryResolver, Injector, NgZone, Type} from
import {IAnnotatedFunction, IAttributes, IAugmentedJQuery, ICompileService, IDirective, IInjectorService, INgModelController, IParseService, IScope} from './angular1';
import {$COMPILE, $INJECTOR, $PARSE, INJECTOR_KEY, LAZY_MODULE_REF, REQUIRE_INJECTOR, REQUIRE_NG_MODEL} from './constants';
import {DowngradeComponentAdapter} from './downgrade_component_adapter';
import {LazyModuleRef, UpgradeAppType, controllerKey, getDowngradedModuleCount, getTypeName, getUpgradeAppType, isFunction, validateInjectionKey} from './util';
import {SyncPromise, Thenable, isThenable} from './promise_util';
import {LazyModuleRef, UpgradeAppType, controllerKey, getDowngradedModuleCount, getTypeName, getUpgradeAppType, validateInjectionKey} from './util';
interface Thenable<T> {
then(callback: (value: T) => any): any;
}
/**
* @description
*
@ -218,42 +215,26 @@ export function downgradeComponent(info: {
/**
* Synchronous promise-like object to wrap parent injectors,
* to preserve the synchronous nature of Angular 1's $compile.
* to preserve the synchronous nature of AngularJS's `$compile`.
*/
class ParentInjectorPromise {
// TODO(issue/24571): remove '!'.
private injector !: Injector;
class ParentInjectorPromise extends SyncPromise<Injector> {
private injectorKey: string = controllerKey(INJECTOR_KEY);
private callbacks: ((injector: Injector) => any)[] = [];
constructor(private element: IAugmentedJQuery) {
super();
// Store the promise on the element.
element.data !(this.injectorKey, this);
}
then(callback: (injector: Injector) => any) {
if (this.injector) {
callback(this.injector);
} else {
this.callbacks.push(callback);
}
}
resolve(injector: Injector) {
this.injector = injector;
resolve(injector: Injector): void {
// Store the real injector on the element.
this.element.data !(this.injectorKey, injector);
// Release the element to prevent memory leaks.
this.element = null !;
// Run the queued callbacks.
this.callbacks.forEach(callback => callback(injector));
this.callbacks.length = 0;
// Resolve the promise.
super.resolve(injector);
}
}
function isThenable<T>(obj: object): obj is Thenable<T> {
return isFunction((obj as any).then);
}

View File

@ -0,0 +1,44 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {isFunction} from './util';
export interface Thenable<T> { then(callback: (value: T) => any): any; }
export function isThenable<T>(obj: unknown): obj is Thenable<T> {
return !!obj && isFunction((obj as any).then);
}
/**
* Synchronous, promise-like object.
*/
export class SyncPromise<T> {
protected value: T|undefined;
private resolved = false;
private callbacks: ((value: T) => unknown)[] = [];
resolve(value: T): void {
// Do nothing, if already resolved.
if (this.resolved) return;
this.value = value;
this.resolved = true;
// Run the queued callbacks.
this.callbacks.forEach(callback => callback(value));
this.callbacks.length = 0;
}
then(callback: (value: T) => unknown): void {
if (this.resolved) {
callback(this.value !);
} else {
this.callbacks.push(callback);
}
}
}

View File

@ -0,0 +1,88 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {SyncPromise, isThenable} from '../src/promise_util';
describe('isThenable()', () => {
it('should return false for primitive values', () => {
expect(isThenable(undefined)).toBe(false);
expect(isThenable(null)).toBe(false);
expect(isThenable(false)).toBe(false);
expect(isThenable(true)).toBe(false);
expect(isThenable(0)).toBe(false);
expect(isThenable(1)).toBe(false);
expect(isThenable('')).toBe(false);
expect(isThenable('foo')).toBe(false);
});
it('should return false if `.then` is not a function', () => {
expect(isThenable([])).toBe(false);
expect(isThenable(['then'])).toBe(false);
expect(isThenable(function() {})).toBe(false);
expect(isThenable({})).toBe(false);
expect(isThenable({then: true})).toBe(false);
expect(isThenable({then: 'not a function'})).toBe(false);
});
it('should return true if `.then` is a function', () => {
expect(isThenable({then: function() {}})).toBe(true);
expect(isThenable({then: () => {}})).toBe(true);
expect(isThenable(Object.assign('thenable', {then: () => {}}))).toBe(true);
});
});
describe('SyncPromise', () => {
it('should call all callbacks once resolved', () => {
const spy1 = jasmine.createSpy('spy1');
const spy2 = jasmine.createSpy('spy2');
const promise = new SyncPromise<string>();
promise.then(spy1);
promise.then(spy2);
expect(spy1).not.toHaveBeenCalled();
expect(spy2).not.toHaveBeenCalled();
promise.resolve('foo');
expect(spy1).toHaveBeenCalledWith('foo');
expect(spy2).toHaveBeenCalledWith('foo');
});
it('should call callbacks immediately if already resolved', () => {
const spy = jasmine.createSpy('spy');
const promise = new SyncPromise<string>();
promise.resolve('foo');
promise.then(spy);
expect(spy).toHaveBeenCalledWith('foo');
});
it('should ignore subsequent calls to `resolve()`', () => {
const spy = jasmine.createSpy('spy');
const promise = new SyncPromise<string>();
promise.then(spy);
promise.resolve('foo');
expect(spy).toHaveBeenCalledWith('foo');
spy.calls.reset();
promise.resolve('bar');
expect(spy).not.toHaveBeenCalled();
promise.then(spy);
promise.resolve('baz');
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith('foo');
});
});