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:
parent
166e98a594
commit
07c1ddc487
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {APP_BASE_HREF, HashLocationStrategy, Location, LOCATION_INITIALIZED, LocationStrategy, PathLocationStrategy, PlatformLocation, ViewportScroller} from '@angular/common';
|
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 {of, Subject} from 'rxjs';
|
||||||
|
|
||||||
import {EmptyOutletComponent} from './components/empty_outlet';
|
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.
|
* pauses. It waits for the hook to be resolved. We then resolve it only in a bootstrap listener.
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RouterInitializer {
|
export class RouterInitializer implements OnDestroy {
|
||||||
private initNavigation: boolean = false;
|
private initNavigation = false;
|
||||||
|
private destroyed = false;
|
||||||
private resultOfPreactivationDone = new Subject<void>();
|
private resultOfPreactivationDone = new Subject<void>();
|
||||||
|
|
||||||
constructor(private injector: Injector) {}
|
constructor(private injector: Injector) {}
|
||||||
|
@ -512,6 +513,11 @@ export class RouterInitializer {
|
||||||
appInitializer(): Promise<any> {
|
appInitializer(): Promise<any> {
|
||||||
const p: Promise<any> = this.injector.get(LOCATION_INITIALIZED, Promise.resolve(null));
|
const p: Promise<any> = this.injector.get(LOCATION_INITIALIZED, Promise.resolve(null));
|
||||||
return p.then(() => {
|
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!;
|
let resolve: Function = null!;
|
||||||
const res = new Promise(r => resolve = r);
|
const res = new Promise(r => resolve = r);
|
||||||
const router = this.injector.get(Router);
|
const router = this.injector.get(Router);
|
||||||
|
@ -566,6 +572,10 @@ export class RouterInitializer {
|
||||||
this.resultOfPreactivationDone.next(null!);
|
this.resultOfPreactivationDone.next(null!);
|
||||||
this.resultOfPreactivationDone.complete();
|
this.resultOfPreactivationDone.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroyed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAppInitializer(r: RouterInitializer) {
|
export function getAppInitializer(r: RouterInitializer) {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {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 {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';
|
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 {EMPTY, Observable, Observer, of, Subscription, SubscriptionLike} from 'rxjs';
|
||||||
import {delay, filter, first, map, mapTo, tap} from 'rxjs/operators';
|
import {delay, filter, first, map, mapTo, tap} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import {RouterInitializer} from '../src/router_module';
|
||||||
import {forEach} from '../src/utils/collection';
|
import {forEach} from '../src/utils/collection';
|
||||||
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
|
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
|
||||||
|
|
||||||
|
@ -6210,6 +6211,42 @@ describe('Integration', () => {
|
||||||
expect(fixture).toContainComponent(Tool2Component, '(e)');
|
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', () => {
|
describe('Testing router options', () => {
|
||||||
|
|
Loading…
Reference in New Issue