fix(router): error if module is destroyed before location is initialized (#42560)

This is something I ran into while working on a fix for the `TestBed` module teardown behavior for #18831. In the `RouterInitializer.appInitializer` we have a callback to the `LOCATION_INITIALIZED` which has to do some DI lookups. The problem is that if the module is destroyed before the location promise resolves, the `Injector.get` calls will fail. This is unlikely to happen in a real app, but it'll show up in unit tests once the test module teardown behavior is fixed.

PR Close #42560
This commit is contained in:
Kristiyan Kostadinov 2021-06-12 08:36:00 +02:00 committed by Dylan Hunn
parent 166e98a594
commit 07c1ddc487
2 changed files with 51 additions and 4 deletions

View File

@ -7,7 +7,7 @@
*/
import {APP_BASE_HREF, HashLocationStrategy, Location, LOCATION_INITIALIZED, LocationStrategy, PathLocationStrategy, PlatformLocation, ViewportScroller} from '@angular/common';
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, Compiler, ComponentRef, Inject, Injectable, InjectionToken, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core';
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, Compiler, ComponentRef, Inject, Injectable, InjectionToken, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, OnDestroy, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core';
import {of, Subject} from 'rxjs';
import {EmptyOutletComponent} from './components/empty_outlet';
@ -503,8 +503,9 @@ export function rootRoute(router: Router): ActivatedRoute {
* pauses. It waits for the hook to be resolved. We then resolve it only in a bootstrap listener.
*/
@Injectable()
export class RouterInitializer {
private initNavigation: boolean = false;
export class RouterInitializer implements OnDestroy {
private initNavigation = false;
private destroyed = false;
private resultOfPreactivationDone = new Subject<void>();
constructor(private injector: Injector) {}
@ -512,6 +513,11 @@ export class RouterInitializer {
appInitializer(): Promise<any> {
const p: Promise<any> = this.injector.get(LOCATION_INITIALIZED, Promise.resolve(null));
return p.then(() => {
// If the injector was destroyed, the DI lookups below will fail.
if (this.destroyed) {
return Promise.resolve(true);
}
let resolve: Function = null!;
const res = new Promise(r => resolve = r);
const router = this.injector.get(Router);
@ -566,6 +572,10 @@ export class RouterInitializer {
this.resultOfPreactivationDone.next(null!);
this.resultOfPreactivationDone.complete();
}
ngOnDestroy() {
this.destroyed = true;
}
}
export function getAppInitializer(r: RouterInitializer) {

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CommonModule, Location, LocationStrategy, PlatformLocation} from '@angular/common';
import {APP_BASE_HREF, CommonModule, Location, LOCATION_INITIALIZED, LocationStrategy, PlatformLocation} from '@angular/common';
import {SpyLocation} from '@angular/common/testing';
import {ChangeDetectionStrategy, Component, EventEmitter, Injectable, NgModule, NgModuleFactoryLoader, NgModuleRef, NgZone, OnDestroy, ViewChild, ɵConsole as Console, ɵNoopNgZone as NoopNgZone} from '@angular/core';
import {ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
@ -16,6 +16,7 @@ import {ActivatedRoute, ActivatedRouteSnapshot, ActivationEnd, ActivationStart,
import {EMPTY, Observable, Observer, of, Subscription, SubscriptionLike} from 'rxjs';
import {delay, filter, first, map, mapTo, tap} from 'rxjs/operators';
import {RouterInitializer} from '../src/router_module';
import {forEach} from '../src/utils/collection';
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
@ -6210,6 +6211,42 @@ describe('Integration', () => {
expect(fixture).toContainComponent(Tool2Component, '(e)');
}));
});
describe('RouterInitializer', () => {
it('should not throw from appInitializer if module is destroyed before location is initialized',
done => {
let resolveInitializer: () => void;
let moduleRef: NgModuleRef<SelfDestructModule>;
@NgModule({
imports: [RouterModule.forRoot([])],
providers: [
{
provide: LOCATION_INITIALIZED,
useValue: new Promise<void>(resolve => resolveInitializer = resolve)
},
{
// Required when running the tests in a browser
provide: APP_BASE_HREF,
useValue: ''
}
]
})
class SelfDestructModule {
constructor(ref: NgModuleRef<SelfDestructModule>, routerInitializer: RouterInitializer) {
moduleRef = ref;
routerInitializer.appInitializer().then(done, done.fail);
}
}
TestBed.resetTestingModule()
.configureTestingModule({imports: [SelfDestructModule], declarations: [SimpleCmp]})
.createComponent(SimpleCmp);
moduleRef!.destroy();
resolveInitializer!();
});
});
});
describe('Testing router options', () => {