https://github.com/angular/angular
Raw File
Tip revision: f24da6baad520f6703bd800927e1438f562fc622 authored by Pawel Kozlowski on 26 October 2022, 18:30:17 UTC
release: cut the v15.0.0-rc.1 release
Tip revision: f24da6b
router_preloader.spec.ts
/**
 * @license
 * Copyright Google LLC 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 {provideLocationMocks} from '@angular/common/testing';
import {Compiler, Component, Injectable, InjectionToken, Injector, NgModule, NgModuleFactory, NgModuleRef, Type} from '@angular/core';
import {fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
import {PreloadAllModules, PreloadingStrategy, provideRoutes, RouterPreloader, withPreloading} 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 {provideRouter} from '../src/provide_router';
import {getLoadedComponent, getLoadedInjector, getLoadedRoutes, getProvidersInjector} from '../src/utils/config';


describe('RouterPreloader', () => {
  @Component({template: ''})
  class LazyLoadedCmp {
  }

  describe('should properly handle', () => {
    beforeEach(() => {
      TestBed.configureTestingModule({
        providers: [
          provideLocationMocks(),
          provideRouter(
              [{path: 'lazy', loadChildren: jasmine.createSpy('expected'), canLoad: ['someGuard']}],
              withPreloading(PreloadAllModules)),
        ]
      });
    });

    it('being destroyed before expected', () => {
      const preloader: RouterPreloader = TestBed.get(RouterPreloader);
      // Calling the RouterPreloader's ngOnDestroy method is done to simulate what would happen if
      // the containing NgModule is destroyed.
      expect(() => preloader.ngOnDestroy()).not.toThrow();
    });
  });

  describe('configurations with canLoad guard', () => {
    @NgModule({
      declarations: [LazyLoadedCmp],
      providers: [provideRoutes([{path: 'LoadedModule1', component: LazyLoadedCmp}])]
    })
    class LoadedModule {
    }

    beforeEach(() => {
      TestBed.configureTestingModule({
        providers: [
          provideLocationMocks(),
          provideRouter(
              [{path: 'lazy', loadChildren: () => LoadedModule, canLoad: ['someGuard']}],
              withPreloading(PreloadAllModules)),
        ],
      });
    });


    it('should not load children',
       fakeAsync(inject([RouterPreloader, Router], (preloader: RouterPreloader, router: Router) => {
         preloader.preload().subscribe(() => {});

         tick();

         const c = router.config;
         expect((c[0] as any)._loadedConfig).not.toBeDefined();
       })));

    it('should not call the preloading method because children will not be loaded anyways',
       fakeAsync(() => {
         const preloader = TestBed.inject(RouterPreloader);
         const preloadingStrategy = TestBed.inject(PreloadingStrategy);
         spyOn(preloadingStrategy, 'preload').and.callThrough();
         preloader.preload().subscribe(() => {});

         tick();
         expect(preloadingStrategy.preload).not.toHaveBeenCalled();
       }));
  });

  describe('should preload configurations', () => {
    let lazySpy: jasmine.Spy;
    beforeEach(() => {
      lazySpy = jasmine.createSpy('expected');
      TestBed.configureTestingModule({
        providers: [
          provideLocationMocks(),
          provideRouter([{path: 'lazy', loadChildren: lazySpy}], withPreloading(PreloadAllModules)),
        ],
      });
    });

    it('should work',
       fakeAsync(inject(
           [RouterPreloader, Router, NgModuleRef],
           (preloader: RouterPreloader, router: Router, testModule: NgModuleRef<any>) => {
             const events: Array<RouteConfigLoadStart|RouteConfigLoadEnd> = [];
             @NgModule({
               declarations: [LazyLoadedCmp],
               imports: [RouterModule.forChild([{path: 'LoadedModule2', component: LazyLoadedCmp}])]
             })
             class LoadedModule2 {
             }

             @NgModule({
               imports: [RouterModule.forChild(
                   [{path: 'LoadedModule1', loadChildren: () => LoadedModule2}])]
             })
             class LoadedModule1 {
             }

             router.events.subscribe(e => {
               if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) {
                 events.push(e);
               }
             });

             lazySpy.and.returnValue(LoadedModule1);
             preloader.preload().subscribe(() => {});

             tick();

             const c = router.config;
             const injector: any = getLoadedInjector(c[0]);
             const loadedRoutes: Route[] = getLoadedRoutes(c[0])!;
             expect(loadedRoutes[0].path).toEqual('LoadedModule1');
             expect(injector.parent).toBe((testModule as any)._r3Injector);

             const injector2: any = getLoadedInjector(loadedRoutes[0]);
             const loadedRoutes2: Route[] = getLoadedRoutes(loadedRoutes[0])!;
             expect(loadedRoutes2[0].path).toEqual('LoadedModule2');
             expect(injector2.parent).toBe(injector);

             expect(events.map(e => e.toString())).toEqual([
               'RouteConfigLoadStart(path: lazy)',
               'RouteConfigLoadEnd(path: lazy)',
               'RouteConfigLoadStart(path: LoadedModule1)',
               'RouteConfigLoadEnd(path: LoadedModule1)',
             ]);
           })));
  });

  it('should handle providers on a route', fakeAsync(() => {
       const TOKEN = new InjectionToken<string>('test token');
       const CHILD_TOKEN = new InjectionToken<string>('test token for child');

       @NgModule({
         imports: [RouterModule.forChild([{path: 'child', redirectTo: ''}])],
         providers: [{provide: CHILD_TOKEN, useValue: 'child'}]
       })
       class Child {
       }

       TestBed.configureTestingModule({
         providers: [
           provideLocationMocks(),
           provideRouter(
               [{
                 path: 'parent',
                 providers: [{provide: TOKEN, useValue: 'parent'}],
                 loadChildren: () => Child,
               }],
               withPreloading(PreloadAllModules)),
         ],
       });

       TestBed.inject(RouterPreloader).preload().subscribe(() => {});

       tick();

       const parentConfig = TestBed.inject(Router).config[0];
       // preloading needs to create the injector
       const providersInjector = getProvidersInjector(parentConfig);
       expect(providersInjector).toBeDefined();
       // Throws error because there is no provider for CHILD_TOKEN here
       expect(() => providersInjector?.get(CHILD_TOKEN)).toThrow();

       const loadedInjector = getLoadedInjector(parentConfig)!;
       // // The loaded injector should be a child of the one created from providers
       expect(loadedInjector.get(TOKEN)).toEqual('parent');
       expect(loadedInjector.get(CHILD_TOKEN)).toEqual('child');
     }));

  describe('should support modules that have already been loaded', () => {
    let lazySpy: jasmine.Spy;
    beforeEach(() => {
      lazySpy = jasmine.createSpy('expected');
      TestBed.configureTestingModule({
        providers: [
          provideLocationMocks(),
          provideRouter([{path: 'lazy', loadChildren: lazySpy}], withPreloading(PreloadAllModules)),
        ],
      });
    });

    it('should work',
       fakeAsync(inject(
           [RouterPreloader, Router, NgModuleRef, Compiler],
           (preloader: RouterPreloader, router: Router, testModule: NgModuleRef<any>,
            compiler: Compiler) => {
             @NgModule()
             class LoadedModule2 {
             }

             const module2 = compiler.compileModuleSync(LoadedModule2).create(null);

             @NgModule({
               imports: [RouterModule.forChild([
                 <Route>{
                   path: 'LoadedModule2',
                   loadChildren: jasmine.createSpy('no'),
                   _loadedRoutes: [{path: 'LoadedModule3', loadChildren: () => LoadedModule3}],
                   _loadedInjector: module2.injector,
                 },
               ])]
             })
             class LoadedModule1 {
             }

             @NgModule({imports: [RouterModule.forChild([])]})
             class LoadedModule3 {
             }

             lazySpy.and.returnValue(LoadedModule1);
             preloader.preload().subscribe(() => {});

             tick();

             const c = router.config;

             const injector = getLoadedInjector(c[0]) as any;
             const loadedRoutes = getLoadedRoutes(c[0])!;
             expect(injector.parent).toBe((testModule as any)._r3Injector);

             const loadedRoutes2: Route[] = getLoadedRoutes(loadedRoutes[0])!;
             const injector3: any = getLoadedInjector(loadedRoutes2[0]);
             expect(injector3.parent).toBe(module2.injector);
           })));
  });

  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({
        providers: [
          provideLocationMocks(),
          provideRouter([{path: 'lazy', loadChildren: lazyLoadChildrenSpy}]),
          {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> {
           override moduleType: Type<any> = LoadedModule1;
           constructor() {
             super();
           }
           override 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', () => {
    @NgModule({
      declarations: [LazyLoadedCmp],
      imports: [RouterModule.forChild([{path: 'LoadedModule1', component: LazyLoadedCmp}])]
    })
    class LoadedModule {
    }

    beforeEach(() => {
      TestBed.configureTestingModule({
        providers: [
          provideRoutes([
            {path: 'lazy1', loadChildren: jasmine.createSpy('expected1')},
            {path: 'lazy2', loadChildren: () => LoadedModule}
          ]),
          {provide: PreloadingStrategy, useExisting: PreloadAllModules},
        ]
      });
    });


    it('should work',
       fakeAsync(inject([RouterPreloader, Router], (preloader: RouterPreloader, router: Router) => {
         preloader.preload().subscribe(() => {});

         tick();

         const c = router.config;
         expect(getLoadedRoutes(c[0] as any)).not.toBeDefined();
         expect(getLoadedRoutes(c[1])).toBeDefined();
       })));
  });

  describe('should copy loaded configs', () => {
    const configs = [{path: 'LoadedModule1', component: LazyLoadedCmp}];
    @NgModule({declarations: [LazyLoadedCmp], providers: [provideRoutes(configs)]})
    class LoadedModule {
    }

    beforeEach(() => {
      TestBed.configureTestingModule({
        providers: [
          provideLocationMocks(),
          provideRouter(
              [{path: 'lazy1', loadChildren: () => LoadedModule}],
              withPreloading(PreloadAllModules)),
        ],
      });
    });


    it('should work',
       fakeAsync(inject([RouterPreloader, Router], (preloader: RouterPreloader, router: Router) => {
         preloader.preload().subscribe(() => {});

         tick();

         const c = router.config;
         expect(getLoadedRoutes(c[0])).toBeDefined();
         expect(getLoadedRoutes(c[0])).not.toBe(configs);
         expect(getLoadedRoutes(c[0])![0]).not.toBe(configs[0]);
         expect(getLoadedRoutes(c[0])![0].component).toBe(configs[0].component);
       })));
  });

  describe(
      'should work with lazy loaded modules that don\'t provide RouterModule.forChild()', () => {
        @NgModule({
          declarations: [LazyLoadedCmp],
          providers: [provideRoutes([{path: 'LoadedModule1', component: LazyLoadedCmp}])]
        })
        class LoadedModule {
        }

        @NgModule({})
        class EmptyModule {
        }

        beforeEach(() => {
          TestBed.configureTestingModule({
            providers: [
              provideLocationMocks(),
              provideRouter(
                  [{path: 'lazyEmptyModule', loadChildren: () => EmptyModule}],
                  withPreloading(PreloadAllModules)),
            ]
          });
        });

        it('should work', fakeAsync(inject([RouterPreloader], (preloader: RouterPreloader) => {
             preloader.preload().subscribe();
           })));
      });

  describe('should preload loadComponent configs', () => {
    let lazyComponentSpy: jasmine.Spy;
    beforeEach(() => {
      lazyComponentSpy = jasmine.createSpy('expected');
      TestBed.configureTestingModule({
        providers: [
          provideLocationMocks(),
          provideRouter(
              [{path: 'lazy', loadComponent: lazyComponentSpy}], withPreloading(PreloadAllModules)),
        ]
      });
    });

    it('base case', fakeAsync(() => {
         @Component({template: '', standalone: true})
         class LoadedComponent {
         }

         const preloader = TestBed.inject(RouterPreloader);
         lazyComponentSpy.and.returnValue(LoadedComponent);
         preloader.preload().subscribe(() => {});

         tick();

         const component = getLoadedComponent(TestBed.inject(Router).config[0]);
         expect(component).toEqual(LoadedComponent);
       }));

    it('throws error when loadComponent is not standalone', fakeAsync(() => {
         @Component({template: '', standalone: false})
         class LoadedComponent {
         }
         @Injectable({providedIn: 'root'})
         class ErrorTrackingPreloadAllModules implements PreloadingStrategy {
           errors: Error[] = [];
           preload(route: Route, fn: () => Observable<any>): Observable<any> {
             return fn().pipe(catchError((e: Error) => {
               this.errors.push(e);
               return of(null);
             }));
           }
         }

         TestBed.overrideProvider(
             PreloadingStrategy, {useFactory: () => new ErrorTrackingPreloadAllModules()});
         const preloader = TestBed.inject(RouterPreloader);
         lazyComponentSpy.and.returnValue(LoadedComponent);
         preloader.preload().subscribe(() => {});

         tick();
         const strategy = TestBed.inject(PreloadingStrategy) as ErrorTrackingPreloadAllModules;
         expect(strategy.errors[0]?.message).toMatch(/.*lazy.*must be standalone/);
       }));

    it('should recover from errors', fakeAsync(() => {
         @Component({template: '', standalone: true})
         class LoadedComponent {
         }

         const preloader = TestBed.inject(RouterPreloader);
         lazyComponentSpy.and.returnValue(throwError('error loading chunk'));
         preloader.preload().subscribe(() => {});

         tick();

         const router = TestBed.inject(Router);
         const c = router.config;
         expect(lazyComponentSpy.calls.count()).toBe(1);
         expect(getLoadedComponent(c[0])).not.toBeDefined();

         lazyComponentSpy.and.returnValue(LoadedComponent);
         router.navigateByUrl('/lazy');
         tick();
         expect(lazyComponentSpy.calls.count()).toBe(2);
         expect(getLoadedComponent(c[0])).toBeDefined();
       }));

    it('works when there is both loadComponent and loadChildren', fakeAsync(() => {
         @Component({template: '', standalone: true})
         class LoadedComponent {
         }

         @NgModule({
           providers: [
             provideLocationMocks(),
             provideRouter([{path: 'child', component: LoadedComponent}]),
           ],
         })
         class LoadedModule {
         }

         const router = TestBed.inject(Router);
         router.config[0].loadChildren = () => LoadedModule;

         const preloader = TestBed.inject(RouterPreloader);
         lazyComponentSpy.and.returnValue(LoadedComponent);
         preloader.preload().subscribe(() => {});

         tick();

         const component = getLoadedComponent(router.config[0]);
         expect(component).toEqual(LoadedComponent);

         const childRoutes = getLoadedRoutes(router.config[0]);
         expect(childRoutes).toBeDefined();
         expect(childRoutes![0].path).toEqual('child');
       }));

    it('loadComponent does not block loadChildren', fakeAsync(() => {
         @Component({template: '', standalone: true})
         class LoadedComponent {
         }

         lazyComponentSpy.and.returnValue(of(LoadedComponent).pipe(delay(5)));

         @NgModule({
           providers: [
             provideLocationMocks(),
             provideRouter([{
               path: 'child',
               loadChildren: () => of([
                                     {path: 'grandchild', children: []},
                                   ]).pipe(delay(1)),
             }]),
           ]
         })
         class LoadedModule {
         }

         const router = TestBed.inject(Router);
         const baseRoute = router.config[0];
         baseRoute.loadChildren = () => of(LoadedModule).pipe(delay(1));

         const preloader = TestBed.inject(RouterPreloader);
         preloader.preload().subscribe(() => {});

         tick(1);
         // Loading should have started but not completed yet
         expect(getLoadedComponent(baseRoute)).not.toBeDefined();
         const childRoutes = getLoadedRoutes(baseRoute);
         expect(childRoutes).toBeDefined();
         // Loading should have started but not completed yet
         expect(getLoadedRoutes(childRoutes![0])).not.toBeDefined();

         tick(1);
         // Loading should have started but not completed yet
         expect(getLoadedComponent(baseRoute)).not.toBeDefined();
         expect(getLoadedRoutes(childRoutes![0])).toBeDefined();

         tick(3);
         expect(getLoadedComponent(baseRoute)).toBeDefined();
       }));
  });
});
back to top