From b3b5c664147c55c2ea79409d512af937eb128bc1 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Thu, 25 Jul 2019 14:01:32 +0300 Subject: [PATCH] refactor(upgrade): extract promise-related utilities to separate file and add tests (#31840) PR Close #31840 --- .../src/common/src/downgrade_component.ts | 37 ++------ .../upgrade/src/common/src/promise_util.ts | 44 ++++++++++ .../src/common/test/promise_util_spec.ts | 88 +++++++++++++++++++ 3 files changed, 141 insertions(+), 28 deletions(-) create mode 100644 packages/upgrade/src/common/src/promise_util.ts create mode 100644 packages/upgrade/src/common/test/promise_util_spec.ts diff --git a/packages/upgrade/src/common/src/downgrade_component.ts b/packages/upgrade/src/common/src/downgrade_component.ts index 48af1735af..abc7b768bd 100644 --- a/packages/upgrade/src/common/src/downgrade_component.ts +++ b/packages/upgrade/src/common/src/downgrade_component.ts @@ -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 { - 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 { 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(obj: object): obj is Thenable { - return isFunction((obj as any).then); -} diff --git a/packages/upgrade/src/common/src/promise_util.ts b/packages/upgrade/src/common/src/promise_util.ts new file mode 100644 index 0000000000..baa0df0568 --- /dev/null +++ b/packages/upgrade/src/common/src/promise_util.ts @@ -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 { then(callback: (value: T) => any): any; } + +export function isThenable(obj: unknown): obj is Thenable { + return !!obj && isFunction((obj as any).then); +} + +/** + * Synchronous, promise-like object. + */ +export class SyncPromise { + 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); + } + } +} diff --git a/packages/upgrade/src/common/test/promise_util_spec.ts b/packages/upgrade/src/common/test/promise_util_spec.ts new file mode 100644 index 0000000000..1563f0e7a2 --- /dev/null +++ b/packages/upgrade/src/common/test/promise_util_spec.ts @@ -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(); + 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(); + 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(); + + 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'); + }); +});