refactor(upgrade): extract promise-related utilities to separate file and add tests (#31840)
PR Close #31840
This commit is contained in:
parent
82b97280f3
commit
b3b5c66414
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue