mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
2492 lines
81 KiB
TypeScript
2492 lines
81 KiB
TypeScript
/**
|
|
* @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.dev/license
|
|
*/
|
|
|
|
import {LocationStrategy, HashLocationStrategy, Location} from '@angular/common';
|
|
import {
|
|
Injectable,
|
|
DestroyRef,
|
|
inject,
|
|
Component,
|
|
NgModule,
|
|
InjectionToken,
|
|
EnvironmentInjector,
|
|
} from '@angular/core';
|
|
import {TestBed, ComponentFixture} from '@angular/core/testing';
|
|
import {Subject, Observable, of, concat, EMPTY} from 'rxjs';
|
|
import {tap, mapTo, first, takeWhile, last, switchMap} from 'rxjs/operators';
|
|
import {
|
|
ActivatedRouteSnapshot,
|
|
RouterStateSnapshot,
|
|
Event,
|
|
Router,
|
|
NavigationStart,
|
|
RoutesRecognized,
|
|
Route,
|
|
UrlSegment,
|
|
GuardsCheckStart,
|
|
ChildActivationStart,
|
|
ActivationStart,
|
|
GuardsCheckEnd,
|
|
NavigationCancel,
|
|
ResolveStart,
|
|
ResolveEnd,
|
|
ActivationEnd,
|
|
ChildActivationEnd,
|
|
NavigationEnd,
|
|
provideRouter,
|
|
withRouterConfig,
|
|
RedirectCommand,
|
|
RunGuardsAndResolvers,
|
|
Data,
|
|
RouterModule,
|
|
NavigationCancellationCode,
|
|
RouteConfigLoadStart,
|
|
RouteConfigLoadEnd,
|
|
UrlTree,
|
|
CanMatchFn,
|
|
CanActivateFn,
|
|
CanActivateChildFn,
|
|
CanDeactivateFn,
|
|
PartialMatchRouteSnapshot,
|
|
} from '../../src';
|
|
import {wrapIntoObservable} from '../../src/utils/collection';
|
|
import {RouterTestingHarness} from '../../testing';
|
|
import {expect} from '@angular/private/testing/matchers';
|
|
import {
|
|
TeamCmp,
|
|
RootCmp,
|
|
BlankCmp,
|
|
expectEvents,
|
|
RouteCmp,
|
|
RootCmpWithTwoOutlets,
|
|
WrapperCmp,
|
|
ThrowingCmp,
|
|
SimpleCmp,
|
|
TwoOutletsCmp,
|
|
UserCmp,
|
|
ModuleWithBlankCmpAsRoute,
|
|
createRoot,
|
|
advance,
|
|
} from './integration_helpers';
|
|
import {timeout} from '@angular/private/testing';
|
|
|
|
export function guardsIntegrationSuite() {
|
|
describe('guards', () => {
|
|
describe('CanActivate', () => {
|
|
describe('guard completes before emitting a value', () => {
|
|
@Injectable({providedIn: 'root'})
|
|
class CompletesBeforeEmitting {
|
|
private subject$ = new Subject<boolean>();
|
|
|
|
constructor(destroyRef: DestroyRef) {
|
|
destroyRef.onDestroy(() => this.subject$.complete());
|
|
}
|
|
|
|
// Note that this is a simple illustrative case of when an observable
|
|
// completes without emitting a value. In a real-world scenario, this
|
|
// might represent an HTTP request that never emits before the app is
|
|
// destroyed and then completes when the app is destroyed.
|
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
|
return this.subject$;
|
|
}
|
|
}
|
|
|
|
it('should not thrown an unhandled promise rejection', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
const onUnhandledrejection = jasmine.createSpy();
|
|
window.addEventListener('unhandledrejection', onUnhandledrejection);
|
|
|
|
router.resetConfig([
|
|
{path: 'team/:id', component: TeamCmp, canActivate: [CompletesBeforeEmitting]},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/22');
|
|
|
|
// This was previously throwing an error `NG0205: Injector has already been destroyed`.
|
|
fixture.destroy();
|
|
|
|
// Wait until the event task is dispatched.
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
window.removeEventListener('unhandledrejection', onUnhandledrejection);
|
|
|
|
expect(onUnhandledrejection).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('should not activate a route when CanActivate returns false', () => {
|
|
it('works', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
const recordedEvents: Event[] = [];
|
|
router.events.forEach((e) => recordedEvents.push(e));
|
|
|
|
router.resetConfig([{path: 'team/:id', component: TeamCmp, canActivate: [() => false]}]);
|
|
|
|
router.navigateByUrl('/team/22');
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('');
|
|
expectEvents(recordedEvents, [
|
|
[NavigationStart, '/team/22'],
|
|
[RoutesRecognized, '/team/22'],
|
|
[GuardsCheckStart, '/team/22'],
|
|
[ChildActivationStart],
|
|
[ActivationStart],
|
|
[GuardsCheckEnd, '/team/22'],
|
|
[NavigationCancel, '/team/22'],
|
|
]);
|
|
expect((recordedEvents[5] as GuardsCheckEnd).shouldActivate).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('should not activate a route when CanActivate returns false (componentless route)', () => {
|
|
it('works', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'parent',
|
|
canActivate: [() => false],
|
|
children: [{path: 'team/:id', component: TeamCmp}],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('parent/team/22');
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('');
|
|
});
|
|
});
|
|
|
|
describe('should activate a route when CanActivate returns true', () => {
|
|
it('works', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([{path: 'team/:id', component: TeamCmp, canActivate: [() => true]}]);
|
|
|
|
router.navigateByUrl('/team/22');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/22');
|
|
});
|
|
});
|
|
|
|
describe('should work when given a class', () => {
|
|
class AlwaysTrue {
|
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({providers: [AlwaysTrue]});
|
|
});
|
|
|
|
it('works', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([{path: 'team/:id', component: TeamCmp, canActivate: [AlwaysTrue]}]);
|
|
|
|
router.navigateByUrl('/team/22');
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('/team/22');
|
|
});
|
|
});
|
|
|
|
describe('should work when returns an observable', () => {
|
|
it('works', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'team/:id',
|
|
component: TeamCmp,
|
|
canActivate: [
|
|
() =>
|
|
new Observable<boolean>((observer) => {
|
|
observer.next(false);
|
|
}),
|
|
],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/22');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('');
|
|
});
|
|
});
|
|
|
|
describe('should work when returns a promise', () => {
|
|
it('works', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'team/:id',
|
|
component: TeamCmp,
|
|
canActivate: [
|
|
(a) => {
|
|
if (a.params['id'] === '22') {
|
|
return Promise.resolve(true);
|
|
} else {
|
|
return Promise.resolve(false);
|
|
}
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/22');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/22');
|
|
|
|
router.navigateByUrl('/team/33');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/22');
|
|
});
|
|
});
|
|
|
|
describe('should reset the location when cancelling a navigation', () => {
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({
|
|
providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}],
|
|
});
|
|
});
|
|
|
|
it('works', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{path: 'one', component: SimpleCmp},
|
|
{path: 'two', component: SimpleCmp, canActivate: [() => false]},
|
|
]);
|
|
|
|
router.navigateByUrl('/one');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/one');
|
|
|
|
location.go('/two');
|
|
location.historyGo(0);
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/one');
|
|
});
|
|
});
|
|
|
|
describe('should redirect to / when guard returns false', () => {
|
|
it('works', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
router.resetConfig([
|
|
{
|
|
path: '',
|
|
component: SimpleCmp,
|
|
},
|
|
{
|
|
path: 'one',
|
|
component: RouteCmp,
|
|
canActivate: [
|
|
() => {
|
|
inject(Router).navigate(['/']);
|
|
return false;
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
const fixture = TestBed.createComponent(RootCmp);
|
|
router.navigateByUrl('/one');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('');
|
|
expect(fixture.nativeElement).toHaveText('simple');
|
|
});
|
|
});
|
|
|
|
describe('should redirect when guard returns UrlTree', () => {
|
|
it('works', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
const recordedEvents: Event[] = [];
|
|
let cancelEvent: NavigationCancel = null!;
|
|
router.events.forEach((e) => {
|
|
recordedEvents.push(e);
|
|
if (e instanceof NavigationCancel) cancelEvent = e;
|
|
});
|
|
router.resetConfig([
|
|
{path: '', component: SimpleCmp},
|
|
{
|
|
path: 'one',
|
|
component: RouteCmp,
|
|
canActivate: [() => inject(Router).parseUrl('/redirected')],
|
|
},
|
|
{path: 'redirected', component: SimpleCmp},
|
|
]);
|
|
|
|
const fixture = TestBed.createComponent(RootCmp);
|
|
router.navigateByUrl('/one');
|
|
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('/redirected');
|
|
expect(fixture.nativeElement).toHaveText('simple');
|
|
expect(cancelEvent && cancelEvent.reason).toBe(
|
|
'NavigationCancelingError: Redirecting to "/redirected"',
|
|
);
|
|
expectEvents(recordedEvents, [
|
|
[NavigationStart, '/one'],
|
|
[RoutesRecognized, '/one'],
|
|
[GuardsCheckStart, '/one'],
|
|
[ChildActivationStart, undefined],
|
|
[ActivationStart, undefined],
|
|
[NavigationCancel, '/one'],
|
|
[NavigationStart, '/redirected'],
|
|
[RoutesRecognized, '/redirected'],
|
|
[GuardsCheckStart, '/redirected'],
|
|
[ChildActivationStart, undefined],
|
|
[ActivationStart, undefined],
|
|
[GuardsCheckEnd, '/redirected'],
|
|
[ResolveStart, '/redirected'],
|
|
[ResolveEnd, '/redirected'],
|
|
[ActivationEnd, undefined],
|
|
[ChildActivationEnd, undefined],
|
|
[NavigationEnd, '/redirected'],
|
|
]);
|
|
});
|
|
|
|
it('works with root url', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
const recordedEvents: Event[] = [];
|
|
let cancelEvent: NavigationCancel = null!;
|
|
router.events.forEach((e: any) => {
|
|
recordedEvents.push(e);
|
|
if (e instanceof NavigationCancel) cancelEvent = e;
|
|
});
|
|
router.resetConfig([
|
|
{path: '', component: SimpleCmp},
|
|
{
|
|
path: 'one',
|
|
component: RouteCmp,
|
|
canActivate: [() => inject(Router).parseUrl('/')],
|
|
},
|
|
]);
|
|
|
|
const fixture = TestBed.createComponent(RootCmp);
|
|
router.navigateByUrl('/one');
|
|
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('');
|
|
expect(fixture.nativeElement).toHaveText('simple');
|
|
expect(cancelEvent && cancelEvent.reason).toBe(
|
|
'NavigationCancelingError: Redirecting to "/"',
|
|
);
|
|
expectEvents(recordedEvents, [
|
|
[NavigationStart, '/one'],
|
|
[RoutesRecognized, '/one'],
|
|
[GuardsCheckStart, '/one'],
|
|
[ChildActivationStart, undefined],
|
|
[ActivationStart, undefined],
|
|
[NavigationCancel, '/one'],
|
|
[NavigationStart, '/'],
|
|
[RoutesRecognized, '/'],
|
|
[GuardsCheckStart, '/'],
|
|
[ChildActivationStart, undefined],
|
|
[ActivationStart, undefined],
|
|
[GuardsCheckEnd, '/'],
|
|
[ResolveStart, '/'],
|
|
[ResolveEnd, '/'],
|
|
[ActivationEnd, undefined],
|
|
[ChildActivationEnd, undefined],
|
|
[NavigationEnd, '/'],
|
|
]);
|
|
});
|
|
|
|
it('replaces URL when URL is updated eagerly so back button can still work', async () => {
|
|
TestBed.configureTestingModule({
|
|
providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))],
|
|
});
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
router.resetConfig([
|
|
{path: '', component: SimpleCmp},
|
|
{
|
|
path: 'one',
|
|
component: RouteCmp,
|
|
canActivate: [() => inject(Router).parseUrl('/redirected')],
|
|
},
|
|
{path: 'redirected', component: SimpleCmp},
|
|
]);
|
|
await createRoot(router, RootCmp);
|
|
router.navigateByUrl('/one');
|
|
const urlChanges: string[] = [];
|
|
location.onUrlChange((change) => {
|
|
urlChanges.push(change);
|
|
});
|
|
|
|
await timeout();
|
|
|
|
expect(location.path()).toEqual('/redirected');
|
|
expect(urlChanges).toEqual(['/one', '/redirected']);
|
|
location.back();
|
|
await timeout();
|
|
expect(location.path()).toEqual('');
|
|
});
|
|
|
|
it('should resolve navigateByUrl promise after redirect finishes', async () => {
|
|
TestBed.configureTestingModule({
|
|
providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))],
|
|
});
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
let resolvedPath = '';
|
|
router.resetConfig([
|
|
{path: '', component: SimpleCmp},
|
|
{
|
|
path: 'one',
|
|
component: RouteCmp,
|
|
canActivate: [() => inject(Router).parseUrl('/redirected')],
|
|
},
|
|
{path: 'redirected', component: SimpleCmp},
|
|
]);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
router.navigateByUrl('/one').then((v) => {
|
|
resolvedPath = location.path();
|
|
});
|
|
|
|
await timeout();
|
|
expect(resolvedPath).toBe('/redirected');
|
|
});
|
|
|
|
it('can redirect to 404 without changing the URL', async () => {
|
|
TestBed.configureTestingModule({
|
|
providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))],
|
|
});
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
router.resetConfig([
|
|
{path: '', component: SimpleCmp},
|
|
{
|
|
path: 'one',
|
|
component: RouteCmp,
|
|
canActivate: [
|
|
() => new RedirectCommand(router.parseUrl('/404'), {skipLocationChange: true}),
|
|
],
|
|
},
|
|
{path: '404', component: SimpleCmp},
|
|
]);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
router.navigateByUrl('/one');
|
|
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('/one');
|
|
expect(router.url.toString()).toEqual('/404');
|
|
});
|
|
|
|
it('can redirect while changing state object', async () => {
|
|
TestBed.configureTestingModule({
|
|
providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))],
|
|
});
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
router.resetConfig([
|
|
{path: '', component: SimpleCmp},
|
|
{
|
|
path: 'one',
|
|
component: RouteCmp,
|
|
canActivate: [
|
|
() => new RedirectCommand(router.parseUrl('/redirected'), {state: {test: 1}}),
|
|
],
|
|
},
|
|
{path: 'redirected', component: SimpleCmp},
|
|
]);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
router.navigateByUrl('/one');
|
|
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('/redirected');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({test: 1}));
|
|
});
|
|
});
|
|
|
|
it('can redirect to 404 without changing the URL', async () => {
|
|
TestBed.configureTestingModule({
|
|
providers: [
|
|
provideRouter([
|
|
{
|
|
path: 'one',
|
|
component: RouteCmp,
|
|
canActivate: [
|
|
() => {
|
|
const router = inject(Router);
|
|
router.navigateByUrl('/404', {
|
|
browserUrl: router.getCurrentNavigation()?.finalUrl,
|
|
});
|
|
return false;
|
|
},
|
|
],
|
|
},
|
|
{path: '404', component: SimpleCmp},
|
|
]),
|
|
],
|
|
});
|
|
const location = TestBed.inject(Location);
|
|
await RouterTestingHarness.create('/one');
|
|
|
|
expect(location.path()).toEqual('/one');
|
|
expect(TestBed.inject(Router).url.toString()).toEqual('/404');
|
|
});
|
|
|
|
it('can navigate to same internal route with different browser url', async () => {
|
|
TestBed.configureTestingModule({
|
|
providers: [provideRouter([{path: 'one', component: RouteCmp}])],
|
|
});
|
|
const location = TestBed.inject(Location);
|
|
const router = TestBed.inject(Router);
|
|
await RouterTestingHarness.create('/one');
|
|
await router.navigateByUrl('/one', {browserUrl: '/two'});
|
|
|
|
expect(location.path()).toEqual('/two');
|
|
expect(router.url.toString()).toEqual('/one');
|
|
});
|
|
|
|
it('retains browserUrl through UrlTree redirects', async () => {
|
|
TestBed.configureTestingModule({
|
|
providers: [
|
|
provideRouter([
|
|
{
|
|
path: 'one',
|
|
component: RouteCmp,
|
|
canActivate: [() => inject(Router).parseUrl('/404')],
|
|
},
|
|
{path: '404', component: SimpleCmp},
|
|
]),
|
|
],
|
|
});
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
await RouterTestingHarness.create();
|
|
await router.navigateByUrl('/one', {browserUrl: router.parseUrl('abc123')});
|
|
|
|
expect(location.path()).toEqual('/abc123');
|
|
expect(TestBed.inject(Router).url.toString()).toEqual('/404');
|
|
});
|
|
|
|
describe('runGuardsAndResolvers', () => {
|
|
let guardRunCount = 0;
|
|
let resolverRunCount = 0;
|
|
|
|
beforeEach(() => {
|
|
guardRunCount = 0;
|
|
resolverRunCount = 0;
|
|
TestBed.configureTestingModule({
|
|
providers: [{provide: 'resolver', useValue: () => resolverRunCount++}],
|
|
});
|
|
});
|
|
|
|
async function configureRouter(
|
|
router: Router,
|
|
runGuardsAndResolvers: RunGuardsAndResolvers,
|
|
): Promise<ComponentFixture<RootCmpWithTwoOutlets>> {
|
|
const fixture = await createRoot(router, RootCmpWithTwoOutlets);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'a',
|
|
runGuardsAndResolvers,
|
|
component: RouteCmp,
|
|
canActivate: [
|
|
() => {
|
|
guardRunCount++;
|
|
return true;
|
|
},
|
|
],
|
|
resolve: {data: 'resolver'},
|
|
},
|
|
{path: 'b', component: SimpleCmp, outlet: 'right'},
|
|
{
|
|
path: 'c/:param',
|
|
runGuardsAndResolvers,
|
|
component: RouteCmp,
|
|
canActivate: [
|
|
() => {
|
|
guardRunCount++;
|
|
return true;
|
|
},
|
|
],
|
|
resolve: {data: 'resolver'},
|
|
},
|
|
{
|
|
path: 'd/:param',
|
|
component: WrapperCmp,
|
|
runGuardsAndResolvers,
|
|
children: [
|
|
{
|
|
path: 'e/:param',
|
|
component: SimpleCmp,
|
|
canActivate: [
|
|
() => {
|
|
guardRunCount++;
|
|
return true;
|
|
},
|
|
],
|
|
resolve: {data: 'resolver'},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
path: 'throwing',
|
|
runGuardsAndResolvers,
|
|
component: ThrowingCmp,
|
|
canActivate: [
|
|
() => {
|
|
guardRunCount++;
|
|
return true;
|
|
},
|
|
],
|
|
resolve: {data: 'resolver'},
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/a');
|
|
await advance(fixture);
|
|
return fixture;
|
|
}
|
|
|
|
it('should rerun guards and resolvers when params change', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const fixture = await configureRouter(router, 'paramsChange');
|
|
|
|
const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance;
|
|
const recordedData: Data[] = [];
|
|
cmp.route.data.subscribe((data) => recordedData.push(data));
|
|
|
|
expect(guardRunCount).toEqual(1);
|
|
expect(recordedData).toEqual([{data: 0}]);
|
|
|
|
router.navigateByUrl('/a;p=1');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(2);
|
|
expect(recordedData).toEqual([{data: 0}, {data: 1}]);
|
|
|
|
router.navigateByUrl('/a;p=2');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(3);
|
|
expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}]);
|
|
|
|
router.navigateByUrl('/a;p=2?q=1');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(3);
|
|
expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}]);
|
|
});
|
|
|
|
it('should rerun guards and resolvers when query params change', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const fixture = await configureRouter(router, 'paramsOrQueryParamsChange');
|
|
|
|
const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance;
|
|
const recordedData: Data[] = [];
|
|
cmp.route.data.subscribe((data) => recordedData.push(data));
|
|
|
|
expect(guardRunCount).toEqual(1);
|
|
expect(recordedData).toEqual([{data: 0}]);
|
|
|
|
router.navigateByUrl('/a;p=1');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(2);
|
|
expect(recordedData).toEqual([{data: 0}, {data: 1}]);
|
|
|
|
router.navigateByUrl('/a;p=2');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(3);
|
|
expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}]);
|
|
|
|
router.navigateByUrl('/a;p=2?q=1');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(4);
|
|
expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}, {data: 3}]);
|
|
|
|
router.navigateByUrl('/a;p=2(right:b)?q=1');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(4);
|
|
expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}, {data: 3}]);
|
|
});
|
|
|
|
it('should always rerun guards and resolvers', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const fixture = await configureRouter(router, 'always');
|
|
|
|
const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance;
|
|
const recordedData: Data[] = [];
|
|
cmp.route.data.subscribe((data) => recordedData.push(data));
|
|
|
|
expect(guardRunCount).toEqual(1);
|
|
expect(recordedData).toEqual([{data: 0}]);
|
|
|
|
router.navigateByUrl('/a;p=1');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(2);
|
|
expect(recordedData).toEqual([{data: 0}, {data: 1}]);
|
|
|
|
router.navigateByUrl('/a;p=2');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(3);
|
|
expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}]);
|
|
|
|
router.navigateByUrl('/a;p=2?q=1');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(4);
|
|
expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}, {data: 3}]);
|
|
|
|
router.navigateByUrl('/a;p=2(right:b)?q=1');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(5);
|
|
expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}, {data: 3}, {data: 4}]);
|
|
|
|
// Issue #39030, always running guards and resolvers should not throw
|
|
// when navigating away from a component with a throwing constructor.
|
|
await expectAsync(
|
|
(async () => {
|
|
router.navigateByUrl('/throwing').catch(() => {});
|
|
await advance(fixture);
|
|
router.navigateByUrl('/a;p=1');
|
|
await advance(fixture);
|
|
})(),
|
|
).not.toBeRejected();
|
|
});
|
|
|
|
it('should rerun rerun guards and resolvers when path params change', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const fixture = await configureRouter(router, 'pathParamsChange');
|
|
|
|
const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance;
|
|
const recordedData: Data[] = [];
|
|
cmp.route.data.subscribe((data) => recordedData.push(data));
|
|
|
|
// First navigation has already run
|
|
expect(guardRunCount).toEqual(1);
|
|
expect(recordedData).toEqual([{data: 0}]);
|
|
|
|
// Changing any optional params will not result in running guards or resolvers
|
|
router.navigateByUrl('/a;p=1');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(1);
|
|
expect(recordedData).toEqual([{data: 0}]);
|
|
|
|
router.navigateByUrl('/a;p=2');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(1);
|
|
expect(recordedData).toEqual([{data: 0}]);
|
|
|
|
router.navigateByUrl('/a;p=2?q=1');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(1);
|
|
expect(recordedData).toEqual([{data: 0}]);
|
|
|
|
router.navigateByUrl('/a;p=2(right:b)?q=1');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(1);
|
|
expect(recordedData).toEqual([{data: 0}]);
|
|
|
|
// Change to new route with path param should run guards and resolvers
|
|
router.navigateByUrl('/c/paramValue');
|
|
await advance(fixture);
|
|
|
|
expect(guardRunCount).toEqual(2);
|
|
|
|
// Modifying a path param should run guards and resolvers
|
|
router.navigateByUrl('/c/paramValueChanged');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(3);
|
|
|
|
// Adding optional params should not cause guards/resolvers to run
|
|
router.navigateByUrl('/c/paramValueChanged;p=1?q=2');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(3);
|
|
});
|
|
|
|
it('should rerun when a parent segment changes', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const fixture = await configureRouter(router, 'pathParamsChange');
|
|
|
|
const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance;
|
|
|
|
// Land on an initial page
|
|
router.navigateByUrl('/d/1;dd=11/e/2;dd=22');
|
|
await advance(fixture);
|
|
|
|
expect(guardRunCount).toEqual(2);
|
|
|
|
// Changes cause re-run on the config with the guard
|
|
router.navigateByUrl('/d/1;dd=11/e/3;ee=22');
|
|
await advance(fixture);
|
|
|
|
expect(guardRunCount).toEqual(3);
|
|
|
|
// Changes to the parent also cause re-run
|
|
router.navigateByUrl('/d/2;dd=11/e/3;ee=22');
|
|
await advance(fixture);
|
|
|
|
expect(guardRunCount).toEqual(4);
|
|
});
|
|
|
|
it('should rerun rerun guards and resolvers when path or query params change', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const fixture = await configureRouter(router, 'pathParamsOrQueryParamsChange');
|
|
|
|
const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance;
|
|
const recordedData: Data[] = [];
|
|
cmp.route.data.subscribe((data) => recordedData.push(data));
|
|
|
|
// First navigation has already run
|
|
expect(guardRunCount).toEqual(1);
|
|
expect(recordedData).toEqual([{data: 0}]);
|
|
|
|
// Changing matrix params will not result in running guards or resolvers
|
|
router.navigateByUrl('/a;p=1');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(1);
|
|
expect(recordedData).toEqual([{data: 0}]);
|
|
|
|
router.navigateByUrl('/a;p=2');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(1);
|
|
expect(recordedData).toEqual([{data: 0}]);
|
|
|
|
// Adding query params will re-run guards/resolvers
|
|
router.navigateByUrl('/a;p=2?q=1');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(2);
|
|
expect(recordedData).toEqual([{data: 0}, {data: 1}]);
|
|
|
|
// Changing query params will re-run guards/resolvers
|
|
router.navigateByUrl('/a;p=2?q=2');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(3);
|
|
expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}]);
|
|
});
|
|
|
|
it('should allow a predicate function to determine when to run guards and resolvers', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const fixture = await configureRouter(router, (from, to) => to.paramMap.get('p') === '2');
|
|
|
|
const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance;
|
|
const recordedData: Data[] = [];
|
|
cmp.route.data.subscribe((data) => recordedData.push(data));
|
|
|
|
// First navigation has already run
|
|
expect(guardRunCount).toEqual(1);
|
|
expect(recordedData).toEqual([{data: 0}]);
|
|
|
|
// Adding `p` param shouldn't cause re-run
|
|
router.navigateByUrl('/a;p=1');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(1);
|
|
expect(recordedData).toEqual([{data: 0}]);
|
|
|
|
// Re-run should trigger on p=2
|
|
router.navigateByUrl('/a;p=2');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(2);
|
|
expect(recordedData).toEqual([{data: 0}, {data: 1}]);
|
|
|
|
// Any other changes don't pass the predicate
|
|
router.navigateByUrl('/a;p=3?q=1');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(2);
|
|
expect(recordedData).toEqual([{data: 0}, {data: 1}]);
|
|
|
|
// Changing query params will re-run guards/resolvers
|
|
router.navigateByUrl('/a;p=3?q=2');
|
|
await advance(fixture);
|
|
expect(guardRunCount).toEqual(2);
|
|
expect(recordedData).toEqual([{data: 0}, {data: 1}]);
|
|
});
|
|
});
|
|
|
|
describe('should wait for parent to complete', () => {
|
|
let log: string[];
|
|
|
|
beforeEach(() => {
|
|
log = [];
|
|
});
|
|
|
|
function delayPromise(delay: number): Promise<boolean> {
|
|
let resolve: (val: boolean) => void;
|
|
const promise = new Promise<boolean>((res) => (resolve = res));
|
|
setTimeout(() => resolve(true), delay);
|
|
return promise;
|
|
}
|
|
|
|
it('works', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'parent',
|
|
canActivate: [
|
|
() =>
|
|
delayPromise(10).then(() => {
|
|
log.push('parent');
|
|
return true;
|
|
}),
|
|
],
|
|
children: [
|
|
{
|
|
path: 'child',
|
|
component: SimpleCmp,
|
|
canActivate: [
|
|
() => {
|
|
return delayPromise(5).then(() => {
|
|
log.push('child');
|
|
return true;
|
|
});
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
await router.navigateByUrl('/parent/child');
|
|
expect(log).toEqual(['parent', 'child']);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('CanDeactivate', () => {
|
|
let log: any;
|
|
const recordingDeactivate: CanDeactivateFn<any> = (c, a) => {
|
|
log.push({path: a.routeConfig!.path, component: c});
|
|
return true;
|
|
};
|
|
|
|
beforeEach(() => {
|
|
log = [];
|
|
});
|
|
|
|
describe('should not deactivate a route when CanDeactivate returns false', () => {
|
|
it('works', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'team/:id',
|
|
component: TeamCmp,
|
|
canDeactivate: [
|
|
(c, a) => {
|
|
return a.params['id'] === '22';
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/22');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/22');
|
|
|
|
let successStatus: boolean = false;
|
|
router.navigateByUrl('/team/33')!.then((res) => (successStatus = res));
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/33');
|
|
expect(successStatus).toEqual(true);
|
|
|
|
let canceledStatus: boolean = false;
|
|
router.navigateByUrl('/team/44')!.then((res) => (canceledStatus = res));
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/33');
|
|
expect(canceledStatus).toEqual(false);
|
|
});
|
|
|
|
it('works with componentless routes', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'grandparent',
|
|
canDeactivate: [recordingDeactivate],
|
|
children: [
|
|
{
|
|
path: 'parent',
|
|
canDeactivate: [recordingDeactivate],
|
|
children: [
|
|
{
|
|
path: 'child',
|
|
canDeactivate: [recordingDeactivate],
|
|
children: [
|
|
{
|
|
path: 'simple',
|
|
component: SimpleCmp,
|
|
canDeactivate: [recordingDeactivate],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{path: 'simple', component: SimpleCmp},
|
|
]);
|
|
|
|
router.navigateByUrl('/grandparent/parent/child/simple');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/grandparent/parent/child/simple');
|
|
|
|
router.navigateByUrl('/simple');
|
|
await advance(fixture);
|
|
|
|
const child = fixture.debugElement.children[1].componentInstance;
|
|
|
|
expect(log.map((a: any) => a.path)).toEqual(['simple', 'child', 'parent', 'grandparent']);
|
|
expect(log[0].component instanceof SimpleCmp).toBeTruthy();
|
|
[1, 2, 3].forEach((i) => expect(log[i].component).toBeNull());
|
|
expect(child instanceof SimpleCmp).toBeTruthy();
|
|
expect(child).not.toBe(log[0].component);
|
|
});
|
|
|
|
it('works with aux routes', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'two-outlets',
|
|
component: TwoOutletsCmp,
|
|
children: [
|
|
{path: 'a', component: BlankCmp},
|
|
{
|
|
path: 'b',
|
|
canDeactivate: [recordingDeactivate],
|
|
component: SimpleCmp,
|
|
outlet: 'aux',
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/two-outlets/(a//aux:b)');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/two-outlets/(a//aux:b)');
|
|
|
|
router.navigate(['two-outlets', {outlets: {aux: null}}]);
|
|
await advance(fixture);
|
|
|
|
expect(log.map((a: any) => a.path)).toEqual(['b']);
|
|
expect(location.path()).toEqual('/two-outlets/a');
|
|
});
|
|
|
|
it('works with a nested route', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'team/:id',
|
|
component: TeamCmp,
|
|
children: [
|
|
{path: '', pathMatch: 'full', component: SimpleCmp},
|
|
{
|
|
path: 'user/:name',
|
|
component: UserCmp,
|
|
canDeactivate: [
|
|
(c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => {
|
|
return a.params['name'] === 'victor';
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/22/user/victor');
|
|
await advance(fixture);
|
|
|
|
// this works because we can deactivate victor
|
|
router.navigateByUrl('/team/33');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/33');
|
|
|
|
router.navigateByUrl('/team/33/user/fedor');
|
|
await advance(fixture);
|
|
|
|
// this doesn't work cause we cannot deactivate fedor
|
|
router.navigateByUrl('/team/44');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/33/user/fedor');
|
|
});
|
|
});
|
|
|
|
it('should use correct component to deactivate forChild route', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
@Component({
|
|
selector: 'admin',
|
|
template: '',
|
|
standalone: false,
|
|
})
|
|
class AdminComponent {}
|
|
|
|
@NgModule({
|
|
declarations: [AdminComponent],
|
|
imports: [
|
|
RouterModule.forChild([
|
|
{
|
|
path: '',
|
|
component: AdminComponent,
|
|
canDeactivate: [recordingDeactivate],
|
|
},
|
|
]),
|
|
],
|
|
})
|
|
class LazyLoadedModule {}
|
|
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'a',
|
|
component: WrapperCmp,
|
|
children: [{path: '', pathMatch: 'full', loadChildren: () => LazyLoadedModule}],
|
|
},
|
|
{path: 'b', component: SimpleCmp},
|
|
]);
|
|
|
|
router.navigateByUrl('/a');
|
|
await advance(fixture);
|
|
router.navigateByUrl('/b');
|
|
await advance(fixture);
|
|
|
|
expect(log[0].component).toBeInstanceOf(AdminComponent);
|
|
});
|
|
|
|
it('should not create a route state if navigation is canceled', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'main',
|
|
component: TeamCmp,
|
|
children: [
|
|
{path: 'component1', component: SimpleCmp, canDeactivate: [() => false]},
|
|
{path: 'component2', component: SimpleCmp},
|
|
],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/main/component1');
|
|
await advance(fixture);
|
|
|
|
router.navigateByUrl('/main/component2');
|
|
await advance(fixture);
|
|
|
|
const teamCmp = fixture.debugElement.children[1].componentInstance;
|
|
expect(teamCmp.route.firstChild.url.value[0].path).toEqual('component1');
|
|
expect(location.path()).toEqual('/main/component1');
|
|
});
|
|
|
|
it('should not run CanActivate when CanDeactivate returns false', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'main',
|
|
component: TeamCmp,
|
|
children: [
|
|
{
|
|
path: 'component1',
|
|
component: SimpleCmp,
|
|
canDeactivate: [
|
|
() => {
|
|
log.push('called');
|
|
let resolve: (result: boolean) => void;
|
|
const promise = new Promise((res) => (resolve = res));
|
|
setTimeout(() => resolve(false), 0);
|
|
return promise;
|
|
},
|
|
],
|
|
},
|
|
{
|
|
path: 'component2',
|
|
component: SimpleCmp,
|
|
canActivate: [
|
|
() => {
|
|
log.push('canActivate called');
|
|
return true;
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/main/component1');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/main/component1');
|
|
|
|
router.navigateByUrl('/main/component2');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/main/component1');
|
|
expect(log).toEqual(['called']);
|
|
});
|
|
|
|
it('should call guards every time when navigating to the same url over and over again', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'simple',
|
|
component: SimpleCmp,
|
|
canDeactivate: [
|
|
() => {
|
|
log.push('called');
|
|
return false;
|
|
},
|
|
],
|
|
},
|
|
{path: 'blank', component: BlankCmp},
|
|
]);
|
|
|
|
router.navigateByUrl('/simple');
|
|
await advance(fixture);
|
|
|
|
router.navigateByUrl('/blank');
|
|
await advance(fixture);
|
|
expect(log).toEqual(['called']);
|
|
expect(location.path()).toEqual('/simple');
|
|
|
|
router.navigateByUrl('/blank');
|
|
await advance(fixture);
|
|
expect(log).toEqual(['called', 'called']);
|
|
expect(location.path()).toEqual('/simple');
|
|
});
|
|
|
|
describe('next state', () => {
|
|
let log: string[];
|
|
|
|
class ClassWithNextState {
|
|
canDeactivate(
|
|
component: TeamCmp,
|
|
currentRoute: ActivatedRouteSnapshot,
|
|
currentState: RouterStateSnapshot,
|
|
nextState: RouterStateSnapshot,
|
|
): boolean {
|
|
log.push(currentState.url, nextState.url);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
beforeEach(() => {
|
|
log = [];
|
|
TestBed.configureTestingModule({
|
|
providers: [ClassWithNextState],
|
|
});
|
|
});
|
|
|
|
it('should pass next state as the 4 argument when guard is a class', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'team/:id',
|
|
component: TeamCmp,
|
|
canDeactivate: [
|
|
(
|
|
component: TeamCmp,
|
|
currentRoute: ActivatedRouteSnapshot,
|
|
currentState: RouterStateSnapshot,
|
|
nextState: RouterStateSnapshot,
|
|
) =>
|
|
inject(ClassWithNextState).canDeactivate(
|
|
component,
|
|
currentRoute,
|
|
currentState,
|
|
nextState,
|
|
),
|
|
],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/22');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/22');
|
|
|
|
router.navigateByUrl('/team/33');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/33');
|
|
expect(log).toEqual(['/team/22', '/team/33']);
|
|
});
|
|
|
|
it('should pass next state as the 4 argument when guard is a function', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'team/:id',
|
|
component: TeamCmp,
|
|
canDeactivate: [
|
|
(cmp, currentRoute, currentState, nextState) => {
|
|
log.push(currentState.url, nextState.url);
|
|
return true;
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/22');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/22');
|
|
|
|
router.navigateByUrl('/team/33');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/33');
|
|
expect(log).toEqual(['/team/22', '/team/33']);
|
|
});
|
|
});
|
|
|
|
describe('should work when given a class', () => {
|
|
class AlwaysTrue {
|
|
canDeactivate(): boolean {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({providers: [AlwaysTrue]});
|
|
});
|
|
|
|
it('works', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'team/:id',
|
|
component: TeamCmp,
|
|
canDeactivate: [() => inject(AlwaysTrue).canDeactivate()],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/22');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/22');
|
|
|
|
router.navigateByUrl('/team/33');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/33');
|
|
});
|
|
});
|
|
|
|
describe('should work when returns an observable', () => {
|
|
it('works', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'team/:id',
|
|
component: TeamCmp,
|
|
canDeactivate: [
|
|
() => {
|
|
return new Observable<boolean>((observer) => {
|
|
observer.next(false);
|
|
});
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/22');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/22');
|
|
|
|
router.navigateByUrl('/team/33');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/22');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('CanActivateChild', () => {
|
|
describe('should be invoked when activating a child', () => {
|
|
it('works', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: '',
|
|
canActivateChild: [(a: any) => a.paramMap.get('id') === '22'],
|
|
children: [{path: 'team/:id', component: TeamCmp}],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/22');
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('/team/22');
|
|
|
|
router.navigateByUrl('/team/33')!.catch(() => {});
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('/team/22');
|
|
});
|
|
});
|
|
|
|
it('should find the guard provided in lazy loaded module', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
@Component({
|
|
selector: 'admin',
|
|
template: '<router-outlet></router-outlet>',
|
|
standalone: false,
|
|
})
|
|
class AdminComponent {}
|
|
|
|
@Component({
|
|
selector: 'lazy',
|
|
template: 'lazy-loaded',
|
|
standalone: false,
|
|
})
|
|
class LazyLoadedComponent {}
|
|
|
|
@NgModule({
|
|
declarations: [AdminComponent, LazyLoadedComponent],
|
|
imports: [
|
|
RouterModule.forChild([
|
|
{
|
|
path: '',
|
|
component: AdminComponent,
|
|
children: [
|
|
{
|
|
path: '',
|
|
canActivateChild: [() => true],
|
|
children: [{path: '', component: LazyLoadedComponent}],
|
|
},
|
|
],
|
|
},
|
|
]),
|
|
],
|
|
})
|
|
class LazyLoadedModule {}
|
|
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([{path: 'admin', loadChildren: () => LazyLoadedModule}]);
|
|
|
|
router.navigateByUrl('/admin');
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('/admin');
|
|
expect(fixture.nativeElement).toHaveText('lazy-loaded');
|
|
});
|
|
});
|
|
|
|
describe('CanLoad', () => {
|
|
let canLoadRunCount = 0;
|
|
beforeEach(() => {
|
|
canLoadRunCount = 0;
|
|
TestBed.configureTestingModule({
|
|
providers: [
|
|
{
|
|
provide: () => true,
|
|
useValue: () => {
|
|
canLoadRunCount++;
|
|
return true;
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should not load children when CanLoad returns false', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
|
|
@Component({
|
|
selector: 'lazy',
|
|
template: 'lazy-loaded',
|
|
standalone: false,
|
|
})
|
|
class LazyLoadedComponent {}
|
|
|
|
@NgModule({
|
|
declarations: [LazyLoadedComponent],
|
|
imports: [RouterModule.forChild([{path: 'loaded', component: LazyLoadedComponent}])],
|
|
})
|
|
class LoadedModule {}
|
|
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{path: 'lazyFalse', canLoad: [() => false], loadChildren: () => LoadedModule},
|
|
{path: 'lazyTrue', canLoad: [() => true], loadChildren: () => LoadedModule},
|
|
]);
|
|
|
|
const recordedEvents: Event[] = [];
|
|
router.events.forEach((e) => recordedEvents.push(e));
|
|
|
|
// failed navigation
|
|
router.navigateByUrl('/lazyFalse/loaded');
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('');
|
|
|
|
expectEvents(recordedEvents, [
|
|
[NavigationStart, '/lazyFalse/loaded'],
|
|
// [GuardsCheckStart, '/lazyFalse/loaded'],
|
|
[NavigationCancel, '/lazyFalse/loaded'],
|
|
]);
|
|
|
|
expect((recordedEvents[1] as NavigationCancel).code).toBe(
|
|
NavigationCancellationCode.GuardRejected,
|
|
);
|
|
|
|
recordedEvents.splice(0);
|
|
|
|
// successful navigation
|
|
router.navigateByUrl('/lazyTrue/loaded');
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('/lazyTrue/loaded');
|
|
|
|
expectEvents(recordedEvents, [
|
|
[NavigationStart, '/lazyTrue/loaded'],
|
|
[RouteConfigLoadStart],
|
|
[RouteConfigLoadEnd],
|
|
[RoutesRecognized, '/lazyTrue/loaded'],
|
|
[GuardsCheckStart, '/lazyTrue/loaded'],
|
|
[ChildActivationStart],
|
|
[ActivationStart],
|
|
[ChildActivationStart],
|
|
[ActivationStart],
|
|
[GuardsCheckEnd, '/lazyTrue/loaded'],
|
|
[ResolveStart, '/lazyTrue/loaded'],
|
|
[ResolveEnd, '/lazyTrue/loaded'],
|
|
[ActivationEnd],
|
|
[ChildActivationEnd],
|
|
[ActivationEnd],
|
|
[ChildActivationEnd],
|
|
[NavigationEnd, '/lazyTrue/loaded'],
|
|
]);
|
|
});
|
|
|
|
it('should support navigating from within the guard', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'lazyFalse',
|
|
canLoad: [
|
|
() => {
|
|
router.navigate(['blank']);
|
|
return false;
|
|
},
|
|
],
|
|
loadChildren: jasmine.createSpy('lazyFalse'),
|
|
},
|
|
{path: 'blank', component: BlankCmp},
|
|
]);
|
|
|
|
const recordedEvents: Event[] = [];
|
|
router.events.forEach((e) => recordedEvents.push(e));
|
|
|
|
router.navigateByUrl('/lazyFalse/loaded');
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('/blank');
|
|
|
|
expectEvents(recordedEvents, [
|
|
[NavigationStart, '/lazyFalse/loaded'],
|
|
// No GuardCheck events as `canLoad` is a special guard that's not actually part of
|
|
// the guard lifecycle.
|
|
[NavigationCancel, '/lazyFalse/loaded'],
|
|
|
|
[NavigationStart, '/blank'],
|
|
[RoutesRecognized, '/blank'],
|
|
[GuardsCheckStart, '/blank'],
|
|
[ChildActivationStart],
|
|
[ActivationStart],
|
|
[GuardsCheckEnd, '/blank'],
|
|
[ResolveStart, '/blank'],
|
|
[ResolveEnd, '/blank'],
|
|
[ActivationEnd],
|
|
[ChildActivationEnd],
|
|
[NavigationEnd, '/blank'],
|
|
]);
|
|
|
|
expect((recordedEvents[1] as NavigationCancel).code).toBe(
|
|
NavigationCancellationCode.SupersededByNewNavigation,
|
|
);
|
|
});
|
|
|
|
it('should support returning UrlTree from within the guard', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'lazyFalse',
|
|
canLoad: [() => inject(Router).createUrlTree(['blank'])],
|
|
loadChildren: jasmine.createSpy('lazyFalse'),
|
|
},
|
|
{path: 'blank', component: BlankCmp},
|
|
]);
|
|
|
|
const recordedEvents: Event[] = [];
|
|
router.events.forEach((e) => recordedEvents.push(e));
|
|
|
|
router.navigateByUrl('/lazyFalse/loaded');
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('/blank');
|
|
|
|
expectEvents(recordedEvents, [
|
|
[NavigationStart, '/lazyFalse/loaded'],
|
|
// No GuardCheck events as `canLoad` is a special guard that's not actually part of
|
|
// the guard lifecycle.
|
|
[NavigationCancel, '/lazyFalse/loaded'],
|
|
|
|
[NavigationStart, '/blank'],
|
|
[RoutesRecognized, '/blank'],
|
|
[GuardsCheckStart, '/blank'],
|
|
[ChildActivationStart],
|
|
[ActivationStart],
|
|
[GuardsCheckEnd, '/blank'],
|
|
[ResolveStart, '/blank'],
|
|
[ResolveEnd, '/blank'],
|
|
[ActivationEnd],
|
|
[ChildActivationEnd],
|
|
[NavigationEnd, '/blank'],
|
|
]);
|
|
|
|
expect((recordedEvents[1] as NavigationCancel).code).toBe(
|
|
NavigationCancellationCode.Redirect,
|
|
);
|
|
});
|
|
|
|
// Regression where navigateByUrl with false CanLoad no longer resolved `false` value on
|
|
// navigateByUrl promise: https://github.com/angular/angular/issues/26284
|
|
it('should resolve navigateByUrl promise after CanLoad executes', async () => {
|
|
const router = TestBed.inject(Router);
|
|
|
|
@Component({
|
|
selector: 'lazy',
|
|
template: 'lazy-loaded',
|
|
standalone: false,
|
|
})
|
|
class LazyLoadedComponent {}
|
|
|
|
@NgModule({
|
|
declarations: [LazyLoadedComponent],
|
|
imports: [RouterModule.forChild([{path: 'loaded', component: LazyLoadedComponent}])],
|
|
})
|
|
class LazyLoadedModule {}
|
|
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{path: 'lazy-false', canLoad: [() => false], loadChildren: () => LazyLoadedModule},
|
|
{path: 'lazy-true', canLoad: [() => true], loadChildren: () => LazyLoadedModule},
|
|
]);
|
|
|
|
let navFalseResult = true;
|
|
let navTrueResult = false;
|
|
router.navigateByUrl('/lazy-false').then((v) => {
|
|
navFalseResult = v;
|
|
});
|
|
await advance(fixture);
|
|
router.navigateByUrl('/lazy-true').then((v) => {
|
|
navTrueResult = v;
|
|
});
|
|
await advance(fixture);
|
|
|
|
expect(navFalseResult).toBe(false);
|
|
expect(navTrueResult).toBe(true);
|
|
});
|
|
|
|
it('should execute CanLoad only once', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
@Component({
|
|
selector: 'lazy',
|
|
template: 'lazy-loaded',
|
|
standalone: false,
|
|
})
|
|
class LazyLoadedComponent {}
|
|
|
|
@NgModule({
|
|
declarations: [LazyLoadedComponent],
|
|
imports: [RouterModule.forChild([{path: 'loaded', component: LazyLoadedComponent}])],
|
|
})
|
|
class LazyLoadedModule {}
|
|
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'lazy',
|
|
canLoad: [
|
|
() => {
|
|
canLoadRunCount++;
|
|
return true;
|
|
},
|
|
],
|
|
loadChildren: () => LazyLoadedModule,
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/lazy/loaded');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/lazy/loaded');
|
|
expect(canLoadRunCount).toEqual(1);
|
|
|
|
router.navigateByUrl('/');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('');
|
|
|
|
router.navigateByUrl('/lazy/loaded');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/lazy/loaded');
|
|
expect(canLoadRunCount).toEqual(1);
|
|
});
|
|
|
|
it('cancels guard execution when a new navigation happens', async () => {
|
|
@Injectable({providedIn: 'root'})
|
|
class DelayedGuard {
|
|
static delayedExecutions = 0;
|
|
static canLoadCalls = 0;
|
|
canLoad() {
|
|
DelayedGuard.canLoadCalls++;
|
|
return of(true).pipe(
|
|
switchMap((v) => new Promise((r) => setTimeout(r, 10)).then(() => v)),
|
|
tap(() => {
|
|
DelayedGuard.delayedExecutions++;
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
const router = TestBed.inject(Router);
|
|
router.resetConfig([
|
|
{path: 'a', canLoad: [DelayedGuard], loadChildren: () => [], component: SimpleCmp},
|
|
{path: 'team/:id', component: TeamCmp},
|
|
]);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.navigateByUrl('/a');
|
|
await timeout(1);
|
|
// The delayed guard should have started
|
|
expect(DelayedGuard.canLoadCalls).toEqual(1);
|
|
await router.navigateByUrl('/team/1');
|
|
expect(fixture.nativeElement.innerHTML).toContain('team');
|
|
// The delayed guard should not execute the delayed condition because a new navigation
|
|
// cancels the current one and unsubscribes from intermediate results.
|
|
expect(DelayedGuard.delayedExecutions).toEqual(0);
|
|
});
|
|
});
|
|
|
|
describe('should run CanLoad guards concurrently', () => {
|
|
function delayObservable(delayMs: number): Observable<boolean> {
|
|
return of(delayMs).pipe(
|
|
switchMap((v) => new Promise((r) => setTimeout(r, delayMs)).then(() => v)),
|
|
mapTo(true),
|
|
);
|
|
}
|
|
|
|
let log: string[];
|
|
const guard1 = () => {
|
|
return delayObservable(15).pipe(tap({next: () => log.push('guard1')}));
|
|
};
|
|
const guard2 = () => {
|
|
return delayObservable(0).pipe(tap({next: () => log.push('guard2')}));
|
|
};
|
|
const returnFalseAndNavigate = () => {
|
|
log.push('returnFalseAndNavigate');
|
|
inject(Router).navigateByUrl('/redirected');
|
|
return false;
|
|
};
|
|
const returnUrlTree = () => {
|
|
const router = inject(Router);
|
|
return delayObservable(30).pipe(
|
|
mapTo(router.parseUrl('/redirected')),
|
|
tap({next: () => log.push('returnUrlTree')}),
|
|
);
|
|
};
|
|
|
|
beforeEach(() => {
|
|
log = [];
|
|
});
|
|
|
|
it('should only execute canLoad guards of routes being activated', async () => {
|
|
const router = TestBed.inject(Router);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'lazy',
|
|
canLoad: [guard1],
|
|
loadChildren: () => of(ModuleWithBlankCmpAsRoute),
|
|
},
|
|
{path: 'redirected', component: SimpleCmp},
|
|
// canLoad should not run for this route because 'lazy' activates first
|
|
{
|
|
path: '',
|
|
canLoad: [returnFalseAndNavigate],
|
|
loadChildren: () => of(ModuleWithBlankCmpAsRoute),
|
|
},
|
|
]);
|
|
|
|
await router.navigateByUrl('/lazy');
|
|
expect(log.length).toEqual(1);
|
|
expect(log).toEqual(['guard1']);
|
|
});
|
|
|
|
it('should execute canLoad guards', async () => {
|
|
const router = TestBed.inject(Router);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'lazy',
|
|
canLoad: [guard1, guard2],
|
|
loadChildren: () => ModuleWithBlankCmpAsRoute,
|
|
},
|
|
]);
|
|
|
|
await router.navigateByUrl('/lazy');
|
|
|
|
expect(log.length).toEqual(2);
|
|
expect(log).toEqual(['guard2', 'guard1']);
|
|
});
|
|
|
|
it('should redirect with UrlTree if higher priority guards have resolved', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'lazy',
|
|
canLoad: [returnUrlTree, guard1, guard2],
|
|
loadChildren: () => ModuleWithBlankCmpAsRoute,
|
|
},
|
|
{path: 'redirected', component: SimpleCmp},
|
|
]);
|
|
|
|
await router.navigateByUrl('/lazy');
|
|
|
|
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', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'lazy',
|
|
canLoad: [guard1, returnUrlTree],
|
|
loadChildren: () => ModuleWithBlankCmpAsRoute,
|
|
},
|
|
{path: 'redirected', component: SimpleCmp},
|
|
]);
|
|
|
|
await router.navigateByUrl('/lazy');
|
|
|
|
expect(log.length).toEqual(2);
|
|
expect(log).toEqual(['guard1', 'returnUrlTree']);
|
|
expect(location.path()).toEqual('/redirected');
|
|
});
|
|
});
|
|
|
|
describe('order', () => {
|
|
class Logger {
|
|
logs: string[] = [];
|
|
add(thing: string) {
|
|
this.logs.push(thing);
|
|
}
|
|
}
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({
|
|
providers: [Logger],
|
|
});
|
|
});
|
|
|
|
it('should call guards in the right order', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const logger = TestBed.inject(Logger);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: '',
|
|
canActivateChild: [() => (logger.add('canActivateChild_parent'), true)],
|
|
children: [
|
|
{
|
|
path: 'team/:id',
|
|
canActivate: [() => (logger.add('canActivate_team'), true)],
|
|
canDeactivate: [() => (logger.add('canDeactivate_team'), true)],
|
|
component: TeamCmp,
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/22');
|
|
await advance(fixture);
|
|
|
|
router.navigateByUrl('/team/33');
|
|
await advance(fixture);
|
|
|
|
expect(logger.logs).toEqual([
|
|
'canActivateChild_parent',
|
|
'canActivate_team',
|
|
|
|
'canDeactivate_team',
|
|
'canActivateChild_parent',
|
|
'canActivate_team',
|
|
]);
|
|
});
|
|
|
|
it('should call deactivate guards from bottom to top', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const logger = TestBed.inject(Logger);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: '',
|
|
children: [
|
|
{
|
|
path: 'team/:id',
|
|
canDeactivate: [() => (logger.add('canDeactivate_team'), true)],
|
|
children: [
|
|
{
|
|
path: '',
|
|
component: SimpleCmp,
|
|
canDeactivate: [() => (logger.add('canDeactivate_simple'), true)],
|
|
},
|
|
],
|
|
component: TeamCmp,
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/22');
|
|
await advance(fixture);
|
|
|
|
router.navigateByUrl('/team/33');
|
|
await advance(fixture);
|
|
|
|
expect(logger.logs).toEqual(['canDeactivate_simple', 'canDeactivate_team']);
|
|
});
|
|
});
|
|
|
|
describe('canMatch', () => {
|
|
@Injectable({providedIn: 'root'})
|
|
class ConfigurableGuard {
|
|
result: Promise<boolean | UrlTree> | Observable<boolean | UrlTree> | boolean | UrlTree =
|
|
false;
|
|
canMatch() {
|
|
return this.result;
|
|
}
|
|
}
|
|
|
|
it('falls back to second route when canMatch returns false', async () => {
|
|
const router = TestBed.inject(Router);
|
|
router.resetConfig([
|
|
{
|
|
path: 'a',
|
|
canMatch: [() => inject(ConfigurableGuard).canMatch()],
|
|
component: BlankCmp,
|
|
},
|
|
{path: 'a', component: SimpleCmp},
|
|
]);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.navigateByUrl('/a');
|
|
await advance(fixture);
|
|
expect(fixture.nativeElement.innerHTML).toContain('simple');
|
|
});
|
|
it('falls back to second route when canMatch returns EMPTY', async () => {
|
|
const router = TestBed.inject(Router);
|
|
router.resetConfig([
|
|
{
|
|
path: 'a',
|
|
canMatch: [() => EMPTY],
|
|
component: BlankCmp,
|
|
},
|
|
{path: 'a', component: SimpleCmp},
|
|
]);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.navigateByUrl('/a');
|
|
await advance(fixture);
|
|
expect(fixture.nativeElement.innerHTML).toContain('simple');
|
|
});
|
|
|
|
it('uses route when canMatch returns true', async () => {
|
|
const router = TestBed.inject(Router);
|
|
TestBed.inject(ConfigurableGuard).result = Promise.resolve(true);
|
|
router.resetConfig([
|
|
{
|
|
path: 'a',
|
|
canMatch: [() => inject(ConfigurableGuard).canMatch()],
|
|
component: SimpleCmp,
|
|
},
|
|
{path: 'a', component: BlankCmp},
|
|
]);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.navigateByUrl('/a');
|
|
await advance(fixture);
|
|
expect(fixture.nativeElement.innerHTML).toContain('simple');
|
|
});
|
|
|
|
it('can return UrlTree from canMatch guard', async () => {
|
|
const router = TestBed.inject(Router);
|
|
TestBed.inject(ConfigurableGuard).result = Promise.resolve(
|
|
router.createUrlTree(['/team/1']),
|
|
);
|
|
router.resetConfig([
|
|
{
|
|
path: 'a',
|
|
canMatch: [() => inject(ConfigurableGuard).canMatch()],
|
|
component: SimpleCmp,
|
|
},
|
|
{path: 'team/:id', component: TeamCmp},
|
|
]);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.navigateByUrl('/a');
|
|
await advance(fixture);
|
|
expect(fixture.nativeElement.innerHTML).toContain('team');
|
|
});
|
|
|
|
it('can return UrlTree from CanMatchFn guard', async () => {
|
|
const canMatchTeamSection = new InjectionToken('CanMatchTeamSection');
|
|
const canMatchFactory: (router: Router) => CanMatchFn = (router: Router) => () =>
|
|
router.createUrlTree(['/team/1']);
|
|
|
|
TestBed.overrideProvider(canMatchTeamSection, {
|
|
useFactory: canMatchFactory,
|
|
deps: [Router],
|
|
});
|
|
|
|
const router = TestBed.inject(Router);
|
|
|
|
router.resetConfig([
|
|
{path: 'a', canMatch: [canMatchTeamSection], component: SimpleCmp},
|
|
{path: 'team/:id', component: TeamCmp},
|
|
]);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.navigateByUrl('/a');
|
|
await advance(fixture);
|
|
expect(fixture.nativeElement.innerHTML).toContain('team');
|
|
});
|
|
|
|
it('runs canMatch guards provided in lazy module', async () => {
|
|
const router = TestBed.inject(Router);
|
|
@Component({
|
|
selector: 'lazy',
|
|
template: 'lazy-loaded-parent [<router-outlet></router-outlet>]',
|
|
standalone: false,
|
|
})
|
|
class ParentLazyLoadedComponent {}
|
|
|
|
@Component({
|
|
selector: 'lazy',
|
|
template: 'lazy-loaded-child',
|
|
standalone: false,
|
|
})
|
|
class ChildLazyLoadedComponent {}
|
|
@Injectable()
|
|
class LazyCanMatchFalse {
|
|
canMatch() {
|
|
return false;
|
|
}
|
|
}
|
|
@Component({
|
|
template: 'restricted',
|
|
standalone: false,
|
|
})
|
|
class Restricted {}
|
|
@NgModule({
|
|
declarations: [ParentLazyLoadedComponent, ChildLazyLoadedComponent, Restricted],
|
|
providers: [LazyCanMatchFalse],
|
|
imports: [
|
|
RouterModule.forChild([
|
|
{
|
|
path: 'loaded',
|
|
canMatch: [LazyCanMatchFalse],
|
|
component: Restricted,
|
|
children: [{path: 'child', component: Restricted}],
|
|
},
|
|
{
|
|
path: 'loaded',
|
|
component: ParentLazyLoadedComponent,
|
|
children: [{path: 'child', component: ChildLazyLoadedComponent}],
|
|
},
|
|
]),
|
|
],
|
|
})
|
|
class LoadedModule {}
|
|
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]);
|
|
router.navigateByUrl('/lazy/loaded/child');
|
|
await advance(fixture);
|
|
|
|
expect(TestBed.inject(Location).path()).toEqual('/lazy/loaded/child');
|
|
expect(fixture.nativeElement).toHaveText('lazy-loaded-parent [lazy-loaded-child]');
|
|
});
|
|
|
|
it('cancels guard execution when a new navigation happens', async () => {
|
|
@Injectable({providedIn: 'root'})
|
|
class DelayedGuard {
|
|
static delayedExecutions = 0;
|
|
canMatch() {
|
|
return of(true).pipe(
|
|
switchMap((v) => new Promise((r) => setTimeout(r, 10)).then(() => v)),
|
|
tap(() => {
|
|
DelayedGuard.delayedExecutions++;
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
const router = TestBed.inject(Router);
|
|
const delayedGuardSpy = spyOn(TestBed.inject(DelayedGuard), 'canMatch');
|
|
delayedGuardSpy.and.callThrough();
|
|
const configurableMatchSpy = spyOn(TestBed.inject(ConfigurableGuard), 'canMatch');
|
|
configurableMatchSpy.and.callFake(() => {
|
|
router.navigateByUrl('/team/1');
|
|
return false;
|
|
});
|
|
router.resetConfig([
|
|
{path: 'a', canMatch: [ConfigurableGuard, DelayedGuard], component: SimpleCmp},
|
|
{path: 'a', canMatch: [ConfigurableGuard, DelayedGuard], component: SimpleCmp},
|
|
{path: 'a', canMatch: [ConfigurableGuard, DelayedGuard], component: SimpleCmp},
|
|
{path: 'team/:id', component: TeamCmp},
|
|
]);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.navigateByUrl('/a');
|
|
await advance(fixture);
|
|
expect(fixture.nativeElement.innerHTML).toContain('team');
|
|
|
|
expect(configurableMatchSpy.calls.count()).toEqual(1);
|
|
|
|
// The delayed guard should not execute the delayed condition because the other guard
|
|
// initiates a new navigation, which cancels the current one and unsubscribes from
|
|
// intermediate results.
|
|
expect(DelayedGuard.delayedExecutions).toEqual(0);
|
|
// The delayed guard should still have executed once because guards are executed at the
|
|
// same time
|
|
expect(delayedGuardSpy.calls.count()).toEqual(1);
|
|
});
|
|
|
|
it('receives PreMatchRouteSnapshot as third argument', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
let capturedSnapshot: PartialMatchRouteSnapshot | undefined;
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'a/:id',
|
|
canMatch: [
|
|
(route: Route, segments: UrlSegment[], snapshot: PartialMatchRouteSnapshot) => {
|
|
capturedSnapshot = snapshot;
|
|
return true;
|
|
},
|
|
],
|
|
component: SimpleCmp,
|
|
},
|
|
]);
|
|
|
|
await router.navigateByUrl('/a/1?q=2#f');
|
|
expect(capturedSnapshot).toBeDefined();
|
|
expect(capturedSnapshot!.params['id']).toBe('1');
|
|
expect(capturedSnapshot!.queryParams['q']).toBe('2');
|
|
expect(capturedSnapshot!.fragment).toBe('f');
|
|
});
|
|
|
|
it('can redirect based on snapshot params', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'a/:id',
|
|
canMatch: [
|
|
(route: Route, segments: UrlSegment[], snapshot: PartialMatchRouteSnapshot) => {
|
|
const router = inject(Router);
|
|
if (snapshot.params['id'] === '1') {
|
|
return router.parseUrl('/b');
|
|
}
|
|
return true;
|
|
},
|
|
],
|
|
component: SimpleCmp,
|
|
},
|
|
{path: 'b', component: BlankCmp},
|
|
]);
|
|
|
|
await router.navigateByUrl('/a/1');
|
|
await advance(fixture);
|
|
expect(router.url).toEqual('/b');
|
|
|
|
await router.navigateByUrl('/a/2');
|
|
await advance(fixture);
|
|
expect(router.url).toEqual('/a/2');
|
|
});
|
|
});
|
|
|
|
it('should allow guards as functions', async () => {
|
|
@Component({
|
|
template: '',
|
|
})
|
|
class BlankCmp {}
|
|
const router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
const guards = {
|
|
canActivate() {
|
|
return true;
|
|
},
|
|
canDeactivate() {
|
|
return true;
|
|
},
|
|
canActivateChild() {
|
|
return true;
|
|
},
|
|
canMatch() {
|
|
return true;
|
|
},
|
|
canLoad() {
|
|
return true;
|
|
},
|
|
};
|
|
spyOn(guards, 'canActivate').and.callThrough();
|
|
spyOn(guards, 'canActivateChild').and.callThrough();
|
|
spyOn(guards, 'canDeactivate').and.callThrough();
|
|
spyOn(guards, 'canLoad').and.callThrough();
|
|
spyOn(guards, 'canMatch').and.callThrough();
|
|
router.resetConfig([
|
|
{
|
|
path: '',
|
|
component: BlankCmp,
|
|
loadChildren: () => [{path: '', component: BlankCmp}],
|
|
canActivate: [guards.canActivate],
|
|
canActivateChild: [guards.canActivateChild],
|
|
canLoad: [guards.canLoad],
|
|
canDeactivate: [guards.canDeactivate],
|
|
canMatch: [guards.canMatch],
|
|
},
|
|
{
|
|
path: 'other',
|
|
component: BlankCmp,
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/');
|
|
await advance(fixture);
|
|
expect(guards.canMatch).toHaveBeenCalled();
|
|
expect(guards.canLoad).toHaveBeenCalled();
|
|
expect(guards.canActivate).toHaveBeenCalled();
|
|
expect(guards.canActivateChild).toHaveBeenCalled();
|
|
|
|
router.navigateByUrl('/other');
|
|
await advance(fixture);
|
|
expect(guards.canDeactivate).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should allow DI in plain function guards', async () => {
|
|
@Component({
|
|
template: '',
|
|
})
|
|
class BlankCmp {}
|
|
|
|
@Injectable({providedIn: 'root'})
|
|
class State {
|
|
value = true;
|
|
}
|
|
const router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
const guards = {
|
|
canActivate() {
|
|
return inject(State).value;
|
|
},
|
|
canDeactivate() {
|
|
return inject(State).value;
|
|
},
|
|
canActivateChild() {
|
|
return inject(State).value;
|
|
},
|
|
canMatch() {
|
|
return inject(State).value;
|
|
},
|
|
canLoad() {
|
|
return inject(State).value;
|
|
},
|
|
};
|
|
spyOn(guards, 'canActivate').and.callThrough();
|
|
spyOn(guards, 'canActivateChild').and.callThrough();
|
|
spyOn(guards, 'canDeactivate').and.callThrough();
|
|
spyOn(guards, 'canLoad').and.callThrough();
|
|
spyOn(guards, 'canMatch').and.callThrough();
|
|
router.resetConfig([
|
|
{
|
|
path: '',
|
|
component: BlankCmp,
|
|
loadChildren: () => [{path: '', component: BlankCmp}],
|
|
canActivate: [guards.canActivate],
|
|
canActivateChild: [guards.canActivateChild],
|
|
canLoad: [guards.canLoad],
|
|
canDeactivate: [guards.canDeactivate],
|
|
canMatch: [guards.canMatch],
|
|
},
|
|
{
|
|
path: 'other',
|
|
component: BlankCmp,
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/');
|
|
await advance(fixture);
|
|
expect(guards.canMatch).toHaveBeenCalled();
|
|
expect(guards.canLoad).toHaveBeenCalled();
|
|
expect(guards.canActivate).toHaveBeenCalled();
|
|
expect(guards.canActivateChild).toHaveBeenCalled();
|
|
|
|
router.navigateByUrl('/other');
|
|
await advance(fixture);
|
|
expect(guards.canDeactivate).toHaveBeenCalled();
|
|
});
|
|
|
|
it('can run functional guards serially', async () => {
|
|
function runSerially(
|
|
guards: CanActivateFn[] | CanActivateChildFn[],
|
|
): CanActivateFn | CanActivateChildFn {
|
|
return (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
|
const injector = inject(EnvironmentInjector);
|
|
const observables = guards.map((guard) => {
|
|
const guardResult = injector.runInContext(() => guard(route, state));
|
|
return wrapIntoObservable(guardResult).pipe(first());
|
|
});
|
|
return concat(...observables).pipe(
|
|
takeWhile((v) => v === true),
|
|
last(),
|
|
);
|
|
};
|
|
}
|
|
|
|
const guardDone: string[] = [];
|
|
|
|
const guard1: CanActivateFn = () =>
|
|
of(true).pipe(
|
|
switchMap((v) => new Promise((r) => setTimeout(r, 4)).then(() => v)),
|
|
tap(() => guardDone.push('guard1')),
|
|
);
|
|
const guard2: CanActivateFn = () => of(true).pipe(tap(() => guardDone.push('guard2')));
|
|
const guard3: CanActivateFn = () =>
|
|
of(true).pipe(
|
|
switchMap((v) => new Promise((r) => setTimeout(r, 2)).then(() => v)),
|
|
tap(() => guardDone.push('guard3')),
|
|
);
|
|
const guard4: CanActivateFn = () =>
|
|
of(true).pipe(
|
|
switchMap((v) => new Promise((r) => setTimeout(r, 6)).then(() => v)),
|
|
tap(() => guardDone.push('guard4')),
|
|
);
|
|
const router = TestBed.inject(Router);
|
|
router.resetConfig([
|
|
{
|
|
path: '**',
|
|
component: BlankCmp,
|
|
canActivate: [runSerially([guard1, guard2, guard3, guard4])],
|
|
},
|
|
]);
|
|
await router.navigateByUrl('');
|
|
expect(guardDone).toEqual(['guard1', 'guard2', 'guard3', 'guard4']);
|
|
});
|
|
|
|
it('should run in injection context', async () => {
|
|
@Injectable({providedIn: 'root'})
|
|
class MyService {
|
|
canRun = false;
|
|
}
|
|
|
|
let resolveCount = 0;
|
|
const routes = [
|
|
{
|
|
path: 'a',
|
|
children: [],
|
|
resolve: {
|
|
x: () => ++resolveCount,
|
|
},
|
|
runGuardsAndResolvers: () => inject(MyService).canRun,
|
|
},
|
|
];
|
|
const router = TestBed.inject(Router);
|
|
router.resetConfig(routes);
|
|
const service = TestBed.inject(MyService);
|
|
|
|
await router.navigateByUrl('/a');
|
|
expect(router.url).toEqual('/a');
|
|
// Always run on activation
|
|
expect(resolveCount).toBe(1);
|
|
|
|
service.canRun = false;
|
|
await router.navigateByUrl('/a?q=1');
|
|
expect(resolveCount).toBe(1);
|
|
|
|
service.canRun = true;
|
|
await router.navigateByUrl('/a?q=2');
|
|
expect(resolveCount).toBe(2);
|
|
});
|
|
});
|
|
}
|