diff --git a/packages/router/src/apply_redirects.ts b/packages/router/src/apply_redirects.ts index b04ba40bb0..6827c0b005 100644 --- a/packages/router/src/apply_redirects.ts +++ b/packages/router/src/apply_redirects.ts @@ -7,11 +7,12 @@ */ import {Injector, NgModuleRef} from '@angular/core'; -import {EmptyError, from, Observable, Observer, of} from 'rxjs'; -import {catchError, concatAll, every, first, map, mergeMap, tap} from 'rxjs/operators'; +import {EmptyError, Observable, Observer, of} from 'rxjs'; +import {catchError, concatAll, first, map, mergeMap, tap} from 'rxjs/operators'; import {LoadedRouterConfig, Route, Routes} from './config'; import {CanLoadFn} from './interfaces'; +import {prioritizedGuardValue} from './operators/prioritized_guard_value'; import {RouterConfigLoader} from './router_config_loader'; import {defaultUrlMatcher, navigationCancelingError, Params, PRIMARY_OUTLET} from './shared'; import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree'; @@ -321,7 +322,7 @@ class ApplyRedirects { const canLoad = route.canLoad; if (!canLoad || canLoad.length === 0) return of(true); - const obs = from(canLoad).pipe(map((injectionToken: any) => { + const canLoadObservables = canLoad.map((injectionToken: any) => { const guard = moduleInjector.get(injectionToken); let guardVal; if (isCanLoad(guard)) { @@ -332,20 +333,21 @@ class ApplyRedirects { throw new Error('Invalid CanLoad guard'); } return wrapIntoObservable(guardVal); - })); + }); - return obs.pipe( - concatAll(), - tap((result: UrlTree|boolean) => { - if (!isUrlTree(result)) return; + return of(canLoadObservables) + .pipe( + prioritizedGuardValue(), + tap((result: UrlTree|boolean) => { + if (!isUrlTree(result)) return; - const error: Error&{url?: UrlTree} = - navigationCancelingError(`Redirecting to "${this.urlSerializer.serialize(result)}"`); - error.url = result; - throw error; - }), - every(result => result === true), - ); + const error: Error&{url?: UrlTree} = navigationCancelingError( + `Redirecting to "${this.urlSerializer.serialize(result)}"`); + error.url = result; + throw error; + }), + map(result => result === true), + ); } private lineralizeSegments(route: Route, urlTree: UrlTree): Observable { diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index 8b3d5d23c1..369936ddfd 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -14,7 +14,7 @@ import {By} from '@angular/platform-browser/src/dom/debug/by'; import {expect} from '@angular/platform-browser/testing/src/matchers'; import {ActivatedRoute, ActivatedRouteSnapshot, ActivationEnd, ActivationStart, CanActivate, CanDeactivate, ChildActivationEnd, ChildActivationStart, DefaultUrlSerializer, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, Navigation, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ParamMap, Params, PreloadAllModules, PreloadingStrategy, PRIMARY_OUTLET, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouteReuseStrategy, RouterEvent, RouterModule, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlSerializer, UrlTree} from '@angular/router'; import {EMPTY, Observable, Observer, of, Subscription} from 'rxjs'; -import {filter, first, map, tap} from 'rxjs/operators'; +import {delay, filter, first, map, mapTo, tap} from 'rxjs/operators'; import {forEach} from '../src/utils/collection'; import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing'; @@ -3998,6 +3998,112 @@ describe('Integration', () => { }))); }); + describe('should run CanLoad guards concurrently', () => { + function delayObservable(delayMs: number): Observable { + return of(delayMs).pipe(delay(delayMs), mapTo(true)); + } + + @NgModule() + class LoadedModule { + } + + let log: string[]; + + beforeEach(() => { + log = []; + TestBed.configureTestingModule({ + providers: [ + { + provide: 'guard1', + useValue: () => { + return delayObservable(5).pipe(tap({next: () => log.push('guard1')})); + } + }, + { + provide: 'guard2', + useValue: () => { + return delayObservable(0).pipe(tap({next: () => log.push('guard2')})); + } + }, + { + provide: 'returnFalse', + useValue: () => { + log.push('returnFalse'); + return false; + } + }, + { + provide: 'returnUrlTree', + useFactory: (router: Router) => () => { + return delayObservable(15).pipe( + mapTo(router.parseUrl('/redirected')), + tap({next: () => log.push('returnUrlTree')})); + }, + deps: [Router] + }, + ] + }); + }); + + it('should wait for higher priority guards to be resolved', + fakeAsync(inject( + [Router, NgModuleFactoryLoader], + (router: Router, loader: SpyNgModuleFactoryLoader) => { + loader.stubbedModules = {expected: LoadedModule}; + + router.resetConfig( + [{path: 'lazy', canLoad: ['guard1', 'guard2'], loadChildren: 'expected'}]); + + router.navigateByUrl('/lazy'); + tick(5); + + expect(log.length).toEqual(2); + expect(log).toEqual(['guard2', 'guard1']); + }))); + + it('should redirect with UrlTree if higher priority guards have resolved', + fakeAsync(inject( + [Router, NgModuleFactoryLoader, Location], + (router: Router, loader: SpyNgModuleFactoryLoader, location: Location) => { + loader.stubbedModules = {expected: LoadedModule}; + + router.resetConfig([ + { + path: 'lazy', + canLoad: ['returnUrlTree', 'guard1', 'guard2'], + loadChildren: 'expected' + }, + {path: 'redirected', component: SimpleCmp} + ]); + + router.navigateByUrl('/lazy'); + tick(15); + + expect(log.length).toEqual(3); + expect(log).toEqual(['guard2', 'guard1', 'returnUrlTree']); + expect(location.path()).toEqual('/redirected'); + }))); + + it('should redirect with UrlTree if UrlTree is lower priority', + fakeAsync(inject( + [Router, NgModuleFactoryLoader, Location], + (router: Router, loader: SpyNgModuleFactoryLoader, location: Location) => { + loader.stubbedModules = {expected: LoadedModule}; + + router.resetConfig([ + {path: 'lazy', canLoad: ['guard1', 'returnUrlTree'], loadChildren: 'expected'}, + {path: 'redirected', component: SimpleCmp} + ]); + + router.navigateByUrl('/lazy'); + tick(15); + + expect(log.length).toEqual(2); + expect(log).toEqual(['guard1', 'returnUrlTree']); + expect(location.path()).toEqual('/redirected'); + }))); + }); + describe('order', () => { class Logger { logs: string[] = [];