From 995adb2297b552fba8b1aeb5f229004cc0a98b8f Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Thu, 18 Feb 2021 15:52:13 -0800 Subject: [PATCH] test(core): refactor ApplicationInitStatus tests to avoid TestBed side-effects (#33222) Currently TestBed (both ViewEngine and Ivy) invoke `ApplicationInitStatus.runInitializers` as a part of the bootstrap process to mimic real bootstrap steps. This is problematic for the `ApplicationInitStatus` class tests since the `runInitializers` call performed by TestBed interfere with actual tests. This commit updates ApplicationInitStatus tests to interact with the class directly instead of relying on TestBed APIs to retrieve the class though DI. PR Close #33222 --- packages/core/test/application_init_spec.ts | 307 +++++++++----------- 1 file changed, 143 insertions(+), 164 deletions(-) diff --git a/packages/core/test/application_init_spec.ts b/packages/core/test/application_init_spec.ts index 70dc0c84af..d36e6df8fb 100644 --- a/packages/core/test/application_init_spec.ts +++ b/packages/core/test/application_init_spec.ts @@ -5,175 +5,154 @@ * 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 {APP_INITIALIZER, ApplicationInitStatus} from '@angular/core/src/application_init'; -import {Observable, Subscriber} from 'rxjs'; +import {ApplicationInitStatus} from '@angular/core/src/application_init'; +import {EMPTY, Observable, Subscriber} from 'rxjs'; -import {inject, TestBed, waitForAsync} from '../testing'; +describe('ApplicationInitStatus', () => { + let status: ApplicationInitStatus; + const runInitializers = () => + // Cast to `any` to access an internal function for testing purposes. + (status as any).runInitializers(); -{ - describe('ApplicationInitStatus', () => { - describe('no initializers', () => { - it('should return true for `done`', - waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { - (status as any).runInitializers(); - expect(status.done).toBe(true); - }))); - - it('should return a promise that resolves immediately for `donePromise`', - waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { - (status as any).runInitializers(); - status.donePromise.then(() => { - expect(status.done).toBe(true); - }); - }))); + describe('no initializers', () => { + beforeEach(() => { + status = new ApplicationInitStatus([]); }); - describe('with async promise initializers', () => { - let resolve: (result: any) => void; - let reject: (reason?: any) => void; - let promise: Promise; - let initFnInvoked = false; - beforeEach(() => { - promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - TestBed.configureTestingModule({ - providers: [ - {provide: APP_INITIALIZER, multi: true, useValue: () => promise}, - ] - }); - }); - - it('should update the status once all async promise initializers are done', - waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { - // Accessing internal `runInitializers` function of the `ApplicationInitStatus` class - // instance for testing purposes to invoke initializer functions. - (status as any).runInitializers(); - - setTimeout(() => { - initFnInvoked = true; - resolve(null); - }); - - expect(status.done).toBe(false); - status.donePromise.then(() => { - expect(status.done).toBe(true); - expect(initFnInvoked).toBe(true); - }); - }))); - - it('should handle a case when promise is rejected', - waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { - // Accessing internal `runInitializers` function of the `ApplicationInitStatus` class - // instance for testing purposes to invoke initializer functions. - (status as any).runInitializers(); - - setTimeout(() => { - initFnInvoked = true; - reject(); - }); - - expect(status.done).toBe(false); - status.donePromise - .then( - () => fail('`donePromise.then` should not be invoked when promise is rejected')) - .catch(() => { - expect(status.done).toBe(false); - expect(initFnInvoked).toBe(true); - }); - }))); + it('should return true for `done`', () => { + runInitializers(); + expect(status.done).toBe(true); }); - describe('with app initializers represented using observables', () => { - let subscriber: Subscriber; - let observable: Observable; - let initFnInvoked = false; - beforeEach(() => { - observable = new Observable((res) => { - subscriber = res; - }); - TestBed.configureTestingModule({ - providers: [ - {provide: APP_INITIALIZER, multi: true, useValue: () => observable}, - ] - }); - }); - - it('should update the status once all async observable initializers are completed', - waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { - // Accessing internal `runInitializers` function of the `ApplicationInitStatus` class - // instance for testing purposes to invoke initializer functions. - (status as any).runInitializers(); - - setTimeout(() => { - initFnInvoked = true; - subscriber.complete(); - }); - - expect(status.done).toBe(false); - status.donePromise.then(() => { - expect(status.done).toBe(true); - expect(initFnInvoked).toBe(true); - }); - }))); - - it('should update the status once all async observable initializers nexted and completed', - waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { - // Accessing internal `runInitializers` function of the `ApplicationInitStatus` class - // instance for testing purposes to invoke initializer functions. - (status as any).runInitializers(); - - subscriber.next('one'); - subscriber.next('two'); - - setTimeout(() => { - initFnInvoked = true; - subscriber.complete(); - }); - - expect(status.done).toBe(false); - status.donePromise.then(() => { - expect(status.done).toBe(true); - expect(initFnInvoked).toBe(true); - }); - }))); - - it('should update the status if all async observable initializers are completed before runInitializers', - waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { - subscriber.complete(); - // Accessing internal `runInitializers` function of the `ApplicationInitStatus` class - // instance for testing purposes to invoke initializer functions. - (status as any).runInitializers(); - - expect(status.done).toBe(false); - - status.donePromise.then(() => { - expect(status.done).toBe(true); - }); - }))); - - it('should handle a case when observable emits an error', - waitForAsync(inject([ApplicationInitStatus], (status: ApplicationInitStatus) => { - // Accessing internal `runInitializers` function of the `ApplicationInitStatus` class - // instance for testing purposes to invoke initializer functions. - (status as any).runInitializers(); - - setTimeout(() => { - initFnInvoked = true; - subscriber.error(); - }); - - expect(status.done).toBe(false); - status.donePromise - .then( - () => fail( - '`donePromise.then` should not be invoked when observable emits an error')) - .catch(() => { - expect(status.done).toBe(false); - expect(initFnInvoked).toBe(true); - }); - }))); + it('should return a promise that resolves immediately for `donePromise`', async () => { + runInitializers(); + await status.donePromise; + expect(status.done).toBe(true); }); }); -} + + describe('with async promise initializers', () => { + let resolve: (result: any) => void; + let reject: (reason?: any) => void; + let promise: Promise; + let initFnInvoked = false; + + beforeEach(() => { + promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + status = new ApplicationInitStatus([() => promise]); + }); + + it('should update the status once all async promise initializers are done', async () => { + runInitializers(); + + setTimeout(() => { + initFnInvoked = true; + resolve(null); + }); + + expect(status.done).toBe(false); + await status.donePromise; + expect(status.done).toBe(true); + expect(initFnInvoked).toBe(true); + }); + + it('should handle a case when promise is rejected', async () => { + runInitializers(); + + setTimeout(() => { + initFnInvoked = true; + reject(); + }); + + expect(status.done).toBe(false); + try { + await status.donePromise; + fail('donePromise should have been rejected when promise is rejected'); + } catch { + expect(status.done).toBe(false); + expect(initFnInvoked).toBe(true); + } + }); + }); + + describe('with app initializers represented using observables', () => { + let subscriber: Subscriber; + let initFnInvoked = false; + beforeEach(() => { + const observable = new Observable((res) => { + subscriber = res; + }); + status = new ApplicationInitStatus([() => observable]); + }); + + it('should update the status once all async observable initializers are completed', + async () => { + runInitializers(); + + setTimeout(() => { + initFnInvoked = true; + subscriber.complete(); + }); + + expect(status.done).toBe(false); + await status.donePromise; + expect(status.done).toBe(true); + expect(initFnInvoked).toBe(true); + }); + + it('should update the status once all async observable initializers emitted and completed', + async () => { + runInitializers(); + + subscriber.next('one'); + subscriber.next('two'); + + setTimeout(() => { + initFnInvoked = true; + subscriber.complete(); + }); + + await status.donePromise; + expect(status.done).toBe(true); + expect(initFnInvoked).toBe(true); + }); + + it('should update the status if all async observable initializers are completed synchronously', + async () => { + // Create a status instance using an initializer that returns the `EMPTY` Observable + // which completes synchronously upon subscription. + status = new ApplicationInitStatus([() => EMPTY]); + + runInitializers(); + + // Although the Observable completes synchronously, we still queue a promise for + // simplicity. This means that the `done` flag will not be `true` immediately, even + // though there was not actually any asynchronous activity. + expect(status.done).toBe(false); + + await status.donePromise; + expect(status.done).toBe(true); + }); + + it('should handle a case when observable emits an error', async () => { + runInitializers(); + + setTimeout(() => { + initFnInvoked = true; + subscriber.error(); + }); + + expect(status.done).toBe(false); + try { + await status.donePromise; + fail('donePromise should have been rejected when observable emits an error'); + } catch { + expect(status.done).toBe(false); + expect(initFnInvoked).toBe(true); + } + }); + }); +});