fix(router): fix load interaction of navigation and preload strategies (#40389)
Fix router to ensure that a route module is only loaded once especially in relation to the use of preload strategies with delayed or partial loading. Add test to check the interaction of PreloadingStrategy and normal router navigation under differing scenarios. Checking: * Prevention of duplicate loading of modules. related to #26557 * Prevention of duplicate RouteConfigLoad(Start|End) events related to #22842 * Ensuring preload strategy remains active for submodules if needed The selected preload strategy should still decide when to load submodules * Possibility of memory leak with unfinished preload subscription related to #26557 * Ensure that the stored loader promise is cleared so that subsequent load will try the fetch again. * Add error handle error from loadChildren * Ensure we handle error from with NgModule create Fixes #26557 #22842 #26557 PR Close #40389
This commit is contained in:
parent
c47dbbede7
commit
95ad452e54
|
@ -12,7 +12,7 @@
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 3033,
|
"runtime-es2015": 3033,
|
||||||
"main-es2015": 447514,
|
"main-es2015": 448036,
|
||||||
"polyfills-es2015": 52493
|
"polyfills-es2015": 52493
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,9 +21,9 @@
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 3153,
|
"runtime-es2015": 3153,
|
||||||
"main-es2015": 432078,
|
"main-es2015": 432647,
|
||||||
"polyfills-es2015": 52493
|
"polyfills-es2015": 52493
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -39,7 +39,7 @@
|
||||||
"master": {
|
"master": {
|
||||||
"uncompressed": {
|
"uncompressed": {
|
||||||
"runtime-es2015": 2285,
|
"runtime-es2015": 2285,
|
||||||
"main-es2015": 241202,
|
"main-es2015": 241843,
|
||||||
"polyfills-es2015": 36709,
|
"polyfills-es2015": 36709,
|
||||||
"5-es2015": 745
|
"5-es2015": 745
|
||||||
}
|
}
|
||||||
|
@ -66,4 +66,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -280,11 +280,12 @@ class ApplyRedirects {
|
||||||
segments: UrlSegment[], outlet: string): Observable<UrlSegmentGroup> {
|
segments: UrlSegment[], outlet: string): Observable<UrlSegmentGroup> {
|
||||||
if (route.path === '**') {
|
if (route.path === '**') {
|
||||||
if (route.loadChildren) {
|
if (route.loadChildren) {
|
||||||
return this.configLoader.load(ngModule.injector, route)
|
const loaded$ = route._loadedConfig ? of(route._loadedConfig) :
|
||||||
.pipe(map((cfg: LoadedRouterConfig) => {
|
this.configLoader.load(ngModule.injector, route);
|
||||||
route._loadedConfig = cfg;
|
return loaded$.pipe(map((cfg: LoadedRouterConfig) => {
|
||||||
return new UrlSegmentGroup(segments, {});
|
route._loadedConfig = cfg;
|
||||||
}));
|
return new UrlSegmentGroup(segments, {});
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return of(new UrlSegmentGroup(segments, {}));
|
return of(new UrlSegmentGroup(segments, {}));
|
||||||
|
|
|
@ -483,6 +483,11 @@ export interface Route {
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
_loadedConfig?: LoadedRouterConfig;
|
_loadedConfig?: LoadedRouterConfig;
|
||||||
|
/**
|
||||||
|
* Filled for routes with `loadChildren` during load
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_loader$?: Observable<LoadedRouterConfig>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LoadedRouterConfig {
|
export class LoadedRouterConfig {
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Compiler, InjectFlags, InjectionToken, Injector, NgModuleFactory, NgModuleFactoryLoader} from '@angular/core';
|
import {Compiler, InjectFlags, InjectionToken, Injector, NgModuleFactory, NgModuleFactoryLoader} from '@angular/core';
|
||||||
import {from, Observable, of} from 'rxjs';
|
import {ConnectableObservable, from, Observable, of, Subject} from 'rxjs';
|
||||||
import {map, mergeMap} from 'rxjs/operators';
|
import {catchError, map, mergeMap, refCount, tap} from 'rxjs/operators';
|
||||||
|
|
||||||
import {LoadChildren, LoadedRouterConfig, Route} from './config';
|
import {LoadChildren, LoadedRouterConfig, Route} from './config';
|
||||||
import {flatten, wrapIntoObservable} from './utils/collection';
|
import {flatten, wrapIntoObservable} from './utils/collection';
|
||||||
|
@ -28,27 +28,39 @@ export class RouterConfigLoader {
|
||||||
private onLoadEndListener?: (r: Route) => void) {}
|
private onLoadEndListener?: (r: Route) => void) {}
|
||||||
|
|
||||||
load(parentInjector: Injector, route: Route): Observable<LoadedRouterConfig> {
|
load(parentInjector: Injector, route: Route): Observable<LoadedRouterConfig> {
|
||||||
|
if (route._loader$) {
|
||||||
|
return route._loader$;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.onLoadStartListener) {
|
if (this.onLoadStartListener) {
|
||||||
this.onLoadStartListener(route);
|
this.onLoadStartListener(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
const moduleFactory$ = this.loadModuleFactory(route.loadChildren!);
|
const moduleFactory$ = this.loadModuleFactory(route.loadChildren!);
|
||||||
|
const loadRunner = moduleFactory$.pipe(
|
||||||
return moduleFactory$.pipe(map((factory: NgModuleFactory<any>) => {
|
map((factory: NgModuleFactory<any>) => {
|
||||||
if (this.onLoadEndListener) {
|
if (this.onLoadEndListener) {
|
||||||
this.onLoadEndListener(route);
|
this.onLoadEndListener(route);
|
||||||
}
|
}
|
||||||
|
const module = factory.create(parentInjector);
|
||||||
const module = factory.create(parentInjector);
|
// When loading a module that doesn't provide `RouterModule.forChild()` preloader
|
||||||
|
// will get stuck in an infinite loop. The child module's Injector will look to
|
||||||
// When loading a module that doesn't provide `RouterModule.forChild()` preloader will get
|
// its parent `Injector` when it doesn't find any ROUTES so it will return routes
|
||||||
// stuck in an infinite loop. The child module's Injector will look to its parent `Injector`
|
// for it's parent module instead.
|
||||||
// when it doesn't find any ROUTES so it will return routes for it's parent module instead.
|
return new LoadedRouterConfig(
|
||||||
return new LoadedRouterConfig(
|
flatten(
|
||||||
flatten(module.injector.get(ROUTES, undefined, InjectFlags.Self | InjectFlags.Optional))
|
module.injector.get(ROUTES, undefined, InjectFlags.Self | InjectFlags.Optional))
|
||||||
.map(standardizeConfig),
|
.map(standardizeConfig),
|
||||||
module);
|
module);
|
||||||
}));
|
}),
|
||||||
|
catchError((err) => {
|
||||||
|
route._loader$ = undefined;
|
||||||
|
throw err;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// Use custom ConnectableObservable as share in runners pipe increasing the bundle size too much
|
||||||
|
route._loader$ = new ConnectableObservable(loadRunner, () => new Subject<LoadedRouterConfig>())
|
||||||
|
.pipe(refCount());
|
||||||
|
return route._loader$;
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadModuleFactory(loadChildren: LoadChildren): Observable<NgModuleFactory<any>> {
|
private loadModuleFactory(loadChildren: LoadChildren): Observable<NgModuleFactory<any>> {
|
||||||
|
|
|
@ -126,7 +126,8 @@ export class RouterPreloader implements OnDestroy {
|
||||||
|
|
||||||
private preloadConfig(ngModule: NgModuleRef<any>, route: Route): Observable<void> {
|
private preloadConfig(ngModule: NgModuleRef<any>, route: Route): Observable<void> {
|
||||||
return this.preloadingStrategy.preload(route, () => {
|
return this.preloadingStrategy.preload(route, () => {
|
||||||
const loaded$ = this.loader.load(ngModule.injector, route);
|
const loaded$ = route._loadedConfig ? of(route._loadedConfig) :
|
||||||
|
this.loader.load(ngModule.injector, route);
|
||||||
return loaded$.pipe(mergeMap((config: LoadedRouterConfig) => {
|
return loaded$.pipe(mergeMap((config: LoadedRouterConfig) => {
|
||||||
route._loadedConfig = config;
|
route._loadedConfig = config;
|
||||||
return this.processRoutes(config.module, config.routes);
|
return this.processRoutes(config.module, config.routes);
|
||||||
|
|
|
@ -514,6 +514,45 @@ describe('applyRedirects', () => {
|
||||||
expect(loaded).toEqual(['root', 'aux']);
|
expect(loaded).toEqual(['root', 'aux']);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should not try to load any matching configuration if previous load completed',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const loadedConfig =
|
||||||
|
new LoadedRouterConfig([{path: 'a', component: ComponentA}], testModule);
|
||||||
|
let loadCalls = 0;
|
||||||
|
let loaded: string[] = [];
|
||||||
|
const loader = {
|
||||||
|
load: (injector: any, p: Route) => {
|
||||||
|
loadCalls++;
|
||||||
|
return of(loadedConfig)
|
||||||
|
.pipe(
|
||||||
|
delay(100 * loadCalls),
|
||||||
|
tap(() => loaded.push(p.loadChildren! as string)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const config: Routes = [
|
||||||
|
{path: '**', loadChildren: 'children'},
|
||||||
|
];
|
||||||
|
|
||||||
|
applyRedirects(testModule.injector, <any>loader, serializer, tree('xyz/a'), config)
|
||||||
|
.subscribe();
|
||||||
|
expect(loadCalls).toBe(1);
|
||||||
|
tick(50);
|
||||||
|
expect(loaded).toEqual([]);
|
||||||
|
applyRedirects(testModule.injector, <any>loader, serializer, tree('xyz/b'), config)
|
||||||
|
.subscribe();
|
||||||
|
tick(50);
|
||||||
|
expect(loaded).toEqual(['children']);
|
||||||
|
expect(loadCalls).toBe(2);
|
||||||
|
tick(200);
|
||||||
|
applyRedirects(testModule.injector, <any>loader, serializer, tree('xyz/c'), config)
|
||||||
|
.subscribe();
|
||||||
|
tick(50);
|
||||||
|
expect(loadCalls).toBe(2);
|
||||||
|
tick(300);
|
||||||
|
}));
|
||||||
|
|
||||||
it('loads only the first match when two Routes with the same outlet have the same path', () => {
|
it('loads only the first match when two Routes with the same outlet have the same path', () => {
|
||||||
const loadedConfig = new LoadedRouterConfig([{path: '', component: ComponentA}], testModule);
|
const loadedConfig = new LoadedRouterConfig([{path: '', component: ComponentA}], testModule);
|
||||||
let loadCalls = 0;
|
let loadCalls = 0;
|
||||||
|
|
|
@ -6,14 +6,18 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Compiler, Component, NgModule, NgModuleFactoryLoader, NgModuleRef} from '@angular/core';
|
import {Compiler, Component, Injector, NgModule, NgModuleFactory, NgModuleFactoryLoader, NgModuleRef, Type} from '@angular/core';
|
||||||
|
import {resolveComponentResources} from '@angular/core/src/metadata/resource_loading';
|
||||||
import {fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
|
import {fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
|
||||||
import {PreloadAllModules, PreloadingStrategy, RouterPreloader} from '@angular/router';
|
import {PreloadAllModules, PreloadingStrategy, RouterPreloader} from '@angular/router';
|
||||||
|
import {BehaviorSubject, Observable, of, throwError} from 'rxjs';
|
||||||
|
import {catchError, delay, filter, switchMap, take} from 'rxjs/operators';
|
||||||
|
|
||||||
import {Route, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouterModule} from '../index';
|
import {Route, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouterModule} from '../index';
|
||||||
import {LoadedRouterConfig} from '../src/config';
|
import {LoadedRouterConfig} from '../src/config';
|
||||||
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
|
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
|
||||||
|
|
||||||
|
|
||||||
describe('RouterPreloader', () => {
|
describe('RouterPreloader', () => {
|
||||||
@Component({template: ''})
|
@Component({template: ''})
|
||||||
class LazyLoadedCmp {
|
class LazyLoadedCmp {
|
||||||
|
@ -196,6 +200,311 @@ describe('RouterPreloader', () => {
|
||||||
})));
|
})));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('should support preloading strategies', () => {
|
||||||
|
let delayLoadUnPaused: BehaviorSubject<string[]>;
|
||||||
|
let delayLoadObserver$: Observable<string[]>;
|
||||||
|
let events: Array<RouteConfigLoadStart|RouteConfigLoadEnd>;
|
||||||
|
|
||||||
|
const subLoadChildrenSpy = jasmine.createSpy('submodule');
|
||||||
|
const lazyLoadChildrenSpy = jasmine.createSpy('lazymodule');
|
||||||
|
|
||||||
|
const mockPreloaderFactory = (): PreloadingStrategy => {
|
||||||
|
class DelayedPreLoad implements PreloadingStrategy {
|
||||||
|
preload(route: Route, fn: () => Observable<any>): Observable<any> {
|
||||||
|
const routeName =
|
||||||
|
route.loadChildren ? (route.loadChildren as jasmine.Spy).and.identity : 'noChildren';
|
||||||
|
return delayLoadObserver$.pipe(
|
||||||
|
filter(unpauseList => unpauseList.indexOf(routeName) !== -1),
|
||||||
|
take(1),
|
||||||
|
switchMap(() => {
|
||||||
|
return fn().pipe(catchError(() => of(null)));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new DelayedPreLoad();
|
||||||
|
};
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [LazyLoadedCmp],
|
||||||
|
})
|
||||||
|
class SharedModule {
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
SharedModule, RouterModule.forChild([
|
||||||
|
{path: 'LoadedModule1', component: LazyLoadedCmp},
|
||||||
|
{path: 'sub', loadChildren: subLoadChildrenSpy}
|
||||||
|
])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
class LoadedModule1 {
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports:
|
||||||
|
[SharedModule, RouterModule.forChild([{path: 'LoadedModule2', component: LazyLoadedCmp}])]
|
||||||
|
})
|
||||||
|
class LoadedModule2 {
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
delayLoadUnPaused = new BehaviorSubject<string[]>([]);
|
||||||
|
delayLoadObserver$ = delayLoadUnPaused.asObservable();
|
||||||
|
subLoadChildrenSpy.calls.reset();
|
||||||
|
lazyLoadChildrenSpy.calls.reset();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports:
|
||||||
|
[RouterTestingModule.withRoutes([{path: 'lazy', loadChildren: lazyLoadChildrenSpy}])],
|
||||||
|
providers: [{provide: PreloadingStrategy, useFactory: mockPreloaderFactory}]
|
||||||
|
});
|
||||||
|
events = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('without reloading loaded modules', fakeAsync(() => {
|
||||||
|
const preloader = TestBed.inject(RouterPreloader);
|
||||||
|
const router = TestBed.inject(Router);
|
||||||
|
router.events.subscribe(e => {
|
||||||
|
if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) {
|
||||||
|
events.push(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1));
|
||||||
|
|
||||||
|
// App start activation of preloader
|
||||||
|
preloader.preload().subscribe((x) => {});
|
||||||
|
tick();
|
||||||
|
expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
// Initial navigation cause route load
|
||||||
|
router.navigateByUrl('/lazy/LoadedModule1');
|
||||||
|
tick();
|
||||||
|
expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Secondary load or navigation should use same loaded object (
|
||||||
|
// ie this is a noop as the module should already be loaded)
|
||||||
|
delayLoadUnPaused.next(['lazymodule']);
|
||||||
|
tick();
|
||||||
|
expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(subLoadChildrenSpy).toHaveBeenCalledTimes(0);
|
||||||
|
expect(events.map(e => e.toString())).toEqual([
|
||||||
|
'RouteConfigLoadStart(path: lazy)', 'RouteConfigLoadEnd(path: lazy)'
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('and cope with the loader throwing exceptions during module load but allow retry',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const preloader = TestBed.inject(RouterPreloader);
|
||||||
|
const router = TestBed.inject(Router);
|
||||||
|
router.events.subscribe(e => {
|
||||||
|
if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) {
|
||||||
|
events.push(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
lazyLoadChildrenSpy.and.returnValue(
|
||||||
|
throwError('Error: Fake module load error (expectedreload)'));
|
||||||
|
preloader.preload().subscribe((x) => {});
|
||||||
|
tick();
|
||||||
|
expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
delayLoadUnPaused.next(['lazymodule']);
|
||||||
|
tick();
|
||||||
|
expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1));
|
||||||
|
router.navigateByUrl('/lazy/LoadedModule1').catch(() => {
|
||||||
|
fail('navigation should not throw');
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(subLoadChildrenSpy).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
expect(events.map(e => e.toString())).toEqual([
|
||||||
|
'RouteConfigLoadStart(path: lazy)', 'RouteConfigLoadStart(path: lazy)',
|
||||||
|
'RouteConfigLoadEnd(path: lazy)'
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('and cope with the loader throwing exceptions but allow retry', fakeAsync(() => {
|
||||||
|
const preloader = TestBed.inject(RouterPreloader);
|
||||||
|
const router = TestBed.inject(Router);
|
||||||
|
router.events.subscribe(e => {
|
||||||
|
if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) {
|
||||||
|
events.push(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
lazyLoadChildrenSpy.and.returnValue(
|
||||||
|
throwError('Error: Fake module load error (expectedreload)'));
|
||||||
|
preloader.preload().subscribe((x) => {});
|
||||||
|
tick();
|
||||||
|
expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
router.navigateByUrl('/lazy/LoadedModule1').catch((reason) => {
|
||||||
|
expect(reason).toEqual('Error: Fake module load error (expectedreload)');
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1);
|
||||||
|
lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1));
|
||||||
|
router.navigateByUrl('/lazy/LoadedModule1').catch(() => {
|
||||||
|
fail('navigation should not throw');
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(subLoadChildrenSpy).toHaveBeenCalledTimes(0);
|
||||||
|
expect(events.map(e => e.toString())).toEqual([
|
||||||
|
'RouteConfigLoadStart(path: lazy)', 'RouteConfigLoadStart(path: lazy)',
|
||||||
|
'RouteConfigLoadEnd(path: lazy)'
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('without autoloading loading submodules', fakeAsync(() => {
|
||||||
|
const preloader = TestBed.inject(RouterPreloader);
|
||||||
|
const router = TestBed.inject(Router);
|
||||||
|
router.events.subscribe(e => {
|
||||||
|
if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) {
|
||||||
|
events.push(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1));
|
||||||
|
subLoadChildrenSpy.and.returnValue(of(LoadedModule2));
|
||||||
|
|
||||||
|
preloader.preload().subscribe((x) => {});
|
||||||
|
tick();
|
||||||
|
router.navigateByUrl('/lazy/LoadedModule1');
|
||||||
|
tick();
|
||||||
|
expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(subLoadChildrenSpy).toHaveBeenCalledTimes(0);
|
||||||
|
expect(events.map(e => e.toString())).toEqual([
|
||||||
|
'RouteConfigLoadStart(path: lazy)', 'RouteConfigLoadEnd(path: lazy)'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Release submodule to check it does in fact load
|
||||||
|
delayLoadUnPaused.next(['lazymodule', 'submodule']);
|
||||||
|
tick();
|
||||||
|
expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(subLoadChildrenSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(events.map(e => e.toString())).toEqual([
|
||||||
|
'RouteConfigLoadStart(path: lazy)', 'RouteConfigLoadEnd(path: lazy)',
|
||||||
|
'RouteConfigLoadStart(path: sub)', 'RouteConfigLoadEnd(path: sub)'
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('and close the preload obsservable ', fakeAsync(() => {
|
||||||
|
const preloader = TestBed.inject(RouterPreloader);
|
||||||
|
const router = TestBed.inject(Router);
|
||||||
|
router.events.subscribe(e => {
|
||||||
|
if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) {
|
||||||
|
events.push(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1));
|
||||||
|
subLoadChildrenSpy.and.returnValue(of(LoadedModule2));
|
||||||
|
const preloadSubscription = preloader.preload().subscribe((x) => {});
|
||||||
|
|
||||||
|
router.navigateByUrl('/lazy/LoadedModule1');
|
||||||
|
tick();
|
||||||
|
delayLoadUnPaused.next(['lazymodule', 'submodule']);
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(subLoadChildrenSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(preloadSubscription.closed).toBeTruthy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('with overlapping loads from navigation and the preloader', fakeAsync(() => {
|
||||||
|
const preloader = TestBed.inject(RouterPreloader);
|
||||||
|
const router = TestBed.inject(Router);
|
||||||
|
router.events.subscribe(e => {
|
||||||
|
if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) {
|
||||||
|
events.push(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1));
|
||||||
|
subLoadChildrenSpy.and.returnValue(of(LoadedModule2).pipe(delay(5)));
|
||||||
|
preloader.preload().subscribe((x) => {});
|
||||||
|
tick();
|
||||||
|
|
||||||
|
// Load the out modules at start of test and ensure it and only
|
||||||
|
// it is loaded
|
||||||
|
delayLoadUnPaused.next(['lazymodule']);
|
||||||
|
tick();
|
||||||
|
expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(events.map(e => e.toString())).toEqual([
|
||||||
|
'RouteConfigLoadStart(path: lazy)',
|
||||||
|
'RouteConfigLoadEnd(path: lazy)',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Cause the load from router to start (has 5 tick delay)
|
||||||
|
router.navigateByUrl('/lazy/sub/LoadedModule2');
|
||||||
|
tick(); // T1
|
||||||
|
// Cause the load from preloader to start
|
||||||
|
delayLoadUnPaused.next(['lazymodule', 'submodule']);
|
||||||
|
tick(); // T2
|
||||||
|
|
||||||
|
expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(subLoadChildrenSpy).toHaveBeenCalledTimes(1);
|
||||||
|
tick(5); // T2 to T7 enough time for mutiple loads to finish
|
||||||
|
|
||||||
|
expect(subLoadChildrenSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(events.map(e => e.toString())).toEqual([
|
||||||
|
'RouteConfigLoadStart(path: lazy)', 'RouteConfigLoadEnd(path: lazy)',
|
||||||
|
'RouteConfigLoadStart(path: sub)', 'RouteConfigLoadEnd(path: sub)'
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('cope with factory fail from broken modules', fakeAsync(() => {
|
||||||
|
const preloader = TestBed.inject(RouterPreloader);
|
||||||
|
const router = TestBed.inject(Router);
|
||||||
|
router.events.subscribe(e => {
|
||||||
|
if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) {
|
||||||
|
events.push(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class BrokenModuleFactory extends NgModuleFactory<any> {
|
||||||
|
moduleType: Type<any> = LoadedModule1;
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
create(_parentInjector: Injector|null): NgModuleRef<any> {
|
||||||
|
throw 'Error: Broken module';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazyLoadChildrenSpy.and.returnValue(of(new BrokenModuleFactory()));
|
||||||
|
preloader.preload().subscribe((x) => {});
|
||||||
|
tick();
|
||||||
|
expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
router.navigateByUrl('/lazy/LoadedModule1').catch((reason) => {
|
||||||
|
expect(reason).toEqual('Error: Broken module');
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1);
|
||||||
|
lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1));
|
||||||
|
router.navigateByUrl('/lazy/LoadedModule1').catch(() => {
|
||||||
|
fail('navigation should not throw');
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(2);
|
||||||
|
expect(subLoadChildrenSpy).toHaveBeenCalledTimes(0);
|
||||||
|
expect(events.map(e => e.toString())).toEqual([
|
||||||
|
'RouteConfigLoadStart(path: lazy)', 'RouteConfigLoadEnd(path: lazy)',
|
||||||
|
'RouteConfigLoadStart(path: lazy)', 'RouteConfigLoadEnd(path: lazy)'
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
describe('should ignore errors', () => {
|
describe('should ignore errors', () => {
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [LazyLoadedCmp],
|
declarations: [LazyLoadedCmp],
|
||||||
|
|
Loading…
Reference in New Issue