mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(router): split remainder of describes in integration test file (#60313)
to more easily navigate and identify different suites of integration tests, this commit splits them into several different files PR Close #60313
This commit is contained in:
parent
0a9403ec7c
commit
0b6084e1bf
5 changed files with 1608 additions and 1483 deletions
|
|
@ -0,0 +1,187 @@
|
|||
/**
|
||||
* @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 {Injectable, inject as coreInject} from '@angular/core';
|
||||
import {Location} from '@angular/common';
|
||||
import {TestBed, fakeAsync, tick} from '@angular/core/testing';
|
||||
import {
|
||||
Router,
|
||||
ActivatedRouteSnapshot,
|
||||
RouterStateSnapshot,
|
||||
provideRouter,
|
||||
withRouterConfig,
|
||||
NavigationStart,
|
||||
GuardsCheckEnd,
|
||||
} from '@angular/router/src';
|
||||
import {createRoot, SimpleCmp, advance, RootCmp, BlankCmp} from './integration_helpers';
|
||||
|
||||
export function duplicateInFlightNavigationsIntegrationSuite() {
|
||||
describe('duplicate in-flight navigations', () => {
|
||||
@Injectable()
|
||||
class RedirectingGuard {
|
||||
skipLocationChange = false;
|
||||
constructor(private router: Router) {}
|
||||
canActivate() {
|
||||
this.router.navigate(['/simple'], {skipLocationChange: this.skipLocationChange});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: 'in1Second',
|
||||
useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => {
|
||||
let res: any = null;
|
||||
const p = new Promise((_) => (res = _));
|
||||
setTimeout(() => res(true), 1000);
|
||||
return p;
|
||||
},
|
||||
},
|
||||
RedirectingGuard,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset location if a navigation by location is successful', fakeAsync(() => {
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.resetConfig([{path: 'simple', component: SimpleCmp, canActivate: ['in1Second']}]);
|
||||
|
||||
// Trigger two location changes to the same URL.
|
||||
// Because of the guard the order will look as follows:
|
||||
// - location change 'simple'
|
||||
// - start processing the change, start a guard
|
||||
// - location change 'simple'
|
||||
// - the first location change gets canceled, the URL gets reset to '/'
|
||||
// - the second location change gets finished, the URL should be reset to '/simple'
|
||||
location.go('/simple');
|
||||
location.historyGo(0);
|
||||
location.historyGo(0);
|
||||
|
||||
tick(2000);
|
||||
advance(fixture);
|
||||
|
||||
expect(location.path()).toEqual('/simple');
|
||||
}));
|
||||
|
||||
it('should skip duplicate location events', fakeAsync(() => {
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'blocked',
|
||||
component: BlankCmp,
|
||||
canActivate: [() => coreInject(RedirectingGuard).canActivate()],
|
||||
},
|
||||
{path: 'simple', component: SimpleCmp},
|
||||
]);
|
||||
router.navigateByUrl('/simple');
|
||||
advance(fixture);
|
||||
|
||||
location.go('/blocked');
|
||||
location.historyGo(0);
|
||||
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement.innerHTML).toContain('simple');
|
||||
}));
|
||||
|
||||
it('should not cause URL thrashing', async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))],
|
||||
});
|
||||
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
const fixture = TestBed.createComponent(RootCmp);
|
||||
fixture.detectChanges();
|
||||
|
||||
router.resetConfig([
|
||||
{path: 'home', component: SimpleCmp},
|
||||
{
|
||||
path: 'blocked',
|
||||
component: BlankCmp,
|
||||
canActivate: [() => coreInject(RedirectingGuard).canActivate()],
|
||||
},
|
||||
{path: 'simple', component: SimpleCmp},
|
||||
]);
|
||||
|
||||
await router.navigateByUrl('/home');
|
||||
const urlChanges: string[] = [];
|
||||
location.onUrlChange((change) => {
|
||||
urlChanges.push(change);
|
||||
});
|
||||
|
||||
await router.navigateByUrl('/blocked');
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(fixture.nativeElement.innerHTML).toContain('simple');
|
||||
// We do not want the URL to flicker to `/home` between the /blocked and /simple routes
|
||||
expect(urlChanges).toEqual(['/blocked', '/simple']);
|
||||
});
|
||||
|
||||
it('can render a 404 page without changing the URL', fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))],
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
TestBed.inject(RedirectingGuard).skipLocationChange = true;
|
||||
const location = TestBed.inject(Location);
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.resetConfig([
|
||||
{path: 'home', component: SimpleCmp},
|
||||
{
|
||||
path: 'blocked',
|
||||
component: BlankCmp,
|
||||
canActivate: [() => coreInject(RedirectingGuard).canActivate()],
|
||||
},
|
||||
{path: 'simple', redirectTo: '404'},
|
||||
{path: '404', component: SimpleCmp},
|
||||
]);
|
||||
router.navigateByUrl('/home');
|
||||
advance(fixture);
|
||||
|
||||
location.go('/blocked');
|
||||
location.historyGo(0);
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/blocked');
|
||||
expect(fixture.nativeElement.innerHTML).toContain('simple');
|
||||
}));
|
||||
|
||||
it('should accurately track currentNavigation', fakeAsync(() => {
|
||||
const router = TestBed.inject(Router);
|
||||
router.resetConfig([
|
||||
{path: 'one', component: SimpleCmp, canActivate: ['in1Second']},
|
||||
{path: 'two', component: BlankCmp, canActivate: ['in1Second']},
|
||||
]);
|
||||
|
||||
router.events.subscribe((e) => {
|
||||
if (e instanceof NavigationStart) {
|
||||
if (e.url === '/one') {
|
||||
router.navigateByUrl('two');
|
||||
}
|
||||
router.events.subscribe((e) => {
|
||||
if (e instanceof GuardsCheckEnd) {
|
||||
expect(router.getCurrentNavigation()?.extractedUrl.toString()).toEqual('/two');
|
||||
expect(router.getCurrentNavigation()?.extras).toBeDefined();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.navigateByUrl('one');
|
||||
tick(1000);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
/**
|
||||
* @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 {Injectable, inject as coreInject} from '@angular/core';
|
||||
import {Location} from '@angular/common';
|
||||
import {TestBed, fakeAsync, tick, inject} from '@angular/core/testing';
|
||||
import {
|
||||
DefaultUrlSerializer,
|
||||
provideRouter,
|
||||
withRouterConfig,
|
||||
Router,
|
||||
GuardsCheckStart,
|
||||
NavigationStart,
|
||||
RoutesRecognized,
|
||||
Navigation,
|
||||
} from '../../src';
|
||||
import {of} from 'rxjs';
|
||||
import {delay, mapTo} from 'rxjs/operators';
|
||||
import {
|
||||
advance,
|
||||
TeamCmp,
|
||||
RootCmp,
|
||||
BlankCmp,
|
||||
SimpleCmp,
|
||||
AbsoluteSimpleLinkCmp,
|
||||
createRoot,
|
||||
} from './integration_helpers';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
|
||||
export function eagerUrlUpdateStrategyIntegrationSuite() {
|
||||
describe('"eager" urlUpdateStrategy', () => {
|
||||
@Injectable()
|
||||
class AuthGuard {
|
||||
canActivateResult = true;
|
||||
|
||||
canActivate() {
|
||||
return this.canActivateResult;
|
||||
}
|
||||
}
|
||||
@Injectable()
|
||||
class DelayedGuard {
|
||||
canActivate() {
|
||||
return of('').pipe(delay(1000), mapTo(true));
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
const serializer = new DefaultUrlSerializer();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: 'authGuardFail',
|
||||
useValue: (a: any, b: any) => {
|
||||
return new Promise((res) => {
|
||||
setTimeout(() => res(serializer.parse('/login')), 1);
|
||||
});
|
||||
},
|
||||
},
|
||||
AuthGuard,
|
||||
DelayedGuard,
|
||||
provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'})),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should eagerly update the URL', fakeAsync(
|
||||
inject([Router, Location], (router: Router, location: Location) => {
|
||||
const fixture = TestBed.createComponent(RootCmp);
|
||||
advance(fixture);
|
||||
|
||||
router.resetConfig([{path: 'team/:id', component: TeamCmp}]);
|
||||
|
||||
router.navigateByUrl('/team/22');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/team/22');
|
||||
|
||||
expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]');
|
||||
|
||||
router.events.subscribe((e) => {
|
||||
if (!(e instanceof GuardsCheckStart)) {
|
||||
return;
|
||||
}
|
||||
expect(location.path()).toEqual('/team/33');
|
||||
expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]');
|
||||
return of(null);
|
||||
});
|
||||
router.navigateByUrl('/team/33');
|
||||
|
||||
advance(fixture);
|
||||
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
|
||||
}),
|
||||
));
|
||||
|
||||
it('should eagerly update the URL', fakeAsync(
|
||||
inject([Router, Location], (router: Router, location: Location) => {
|
||||
const fixture = TestBed.createComponent(RootCmp);
|
||||
advance(fixture);
|
||||
|
||||
router.resetConfig([
|
||||
{path: 'team/:id', component: SimpleCmp, canActivate: ['authGuardFail']},
|
||||
{path: 'login', component: AbsoluteSimpleLinkCmp},
|
||||
]);
|
||||
|
||||
router.navigateByUrl('/team/22');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/team/22');
|
||||
|
||||
// Redirects to /login
|
||||
advance(fixture, 1);
|
||||
expect(location.path()).toEqual('/login');
|
||||
|
||||
// Perform the same logic again, and it should produce the same result
|
||||
router.navigateByUrl('/team/22');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/team/22');
|
||||
|
||||
// Redirects to /login
|
||||
advance(fixture, 1);
|
||||
expect(location.path()).toEqual('/login');
|
||||
}),
|
||||
));
|
||||
|
||||
it('should eagerly update URL after redirects are applied', fakeAsync(
|
||||
inject([Router, Location], (router: Router, location: Location) => {
|
||||
const fixture = TestBed.createComponent(RootCmp);
|
||||
advance(fixture);
|
||||
|
||||
router.resetConfig([{path: 'team/:id', component: TeamCmp}]);
|
||||
|
||||
router.navigateByUrl('/team/22');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/team/22');
|
||||
|
||||
expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]');
|
||||
|
||||
let urlAtNavStart = '';
|
||||
let urlAtRoutesRecognized = '';
|
||||
router.events.subscribe((e) => {
|
||||
if (e instanceof NavigationStart) {
|
||||
urlAtNavStart = location.path();
|
||||
}
|
||||
if (e instanceof RoutesRecognized) {
|
||||
urlAtRoutesRecognized = location.path();
|
||||
}
|
||||
});
|
||||
|
||||
router.navigateByUrl('/team/33');
|
||||
|
||||
advance(fixture);
|
||||
expect(urlAtNavStart).toBe('/team/22');
|
||||
expect(urlAtRoutesRecognized).toBe('/team/33');
|
||||
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
|
||||
}),
|
||||
));
|
||||
|
||||
it('should set `state`', fakeAsync(
|
||||
inject([Router, Location], (router: Router, location: Location) => {
|
||||
router.resetConfig([
|
||||
{path: '', component: SimpleCmp},
|
||||
{path: 'simple', component: SimpleCmp},
|
||||
]);
|
||||
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
let navigation: Navigation = null!;
|
||||
router.events.subscribe((e) => {
|
||||
if (e instanceof NavigationStart) {
|
||||
navigation = router.getCurrentNavigation()!;
|
||||
}
|
||||
});
|
||||
|
||||
router.navigateByUrl('/simple', {state: {foo: 'bar'}});
|
||||
tick();
|
||||
|
||||
const state = location.getState() as any;
|
||||
expect(state).toEqual({foo: 'bar', navigationId: 2});
|
||||
expect(navigation.extras.state).toBeDefined();
|
||||
expect(navigation.extras.state).toEqual({foo: 'bar'});
|
||||
}),
|
||||
));
|
||||
|
||||
it('can renavigate to rejected URL', fakeAsync(() => {
|
||||
const router = TestBed.inject(Router);
|
||||
const canActivate = TestBed.inject(AuthGuard);
|
||||
const location = TestBed.inject(Location);
|
||||
router.resetConfig([
|
||||
{path: '', component: BlankCmp},
|
||||
{
|
||||
path: 'simple',
|
||||
component: SimpleCmp,
|
||||
canActivate: [() => coreInject(AuthGuard).canActivate()],
|
||||
},
|
||||
]);
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
// Try to navigate to /simple but guard rejects
|
||||
canActivate.canActivateResult = false;
|
||||
router.navigateByUrl('/simple');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('simple');
|
||||
|
||||
// Renavigate to /simple without guard rejection, should succeed.
|
||||
canActivate.canActivateResult = true;
|
||||
router.navigateByUrl('/simple');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/simple');
|
||||
expect(fixture.nativeElement.innerHTML).toContain('simple');
|
||||
}));
|
||||
|
||||
it('can renavigate to same URL during in-flight navigation', fakeAsync(() => {
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
router.resetConfig([
|
||||
{path: '', component: BlankCmp},
|
||||
{
|
||||
path: 'simple',
|
||||
component: SimpleCmp,
|
||||
canActivate: [() => coreInject(DelayedGuard).canActivate()],
|
||||
},
|
||||
]);
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
// Start navigating to /simple, but do not flush the guard delay
|
||||
router.navigateByUrl('/simple');
|
||||
tick();
|
||||
// eager update strategy so URL is already updated.
|
||||
expect(location.path()).toEqual('/simple');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('simple');
|
||||
|
||||
// Start an additional navigation to /simple and ensure at least one of those succeeds.
|
||||
// It's not super important which one gets processed, but in the past, the router would
|
||||
// cancel the in-flight one and not process the new one.
|
||||
router.navigateByUrl('/simple');
|
||||
tick(1000);
|
||||
expect(location.path()).toEqual('/simple');
|
||||
expect(fixture.nativeElement.innerHTML).toContain('simple');
|
||||
}));
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
718
packages/router/test/integration/navigation.spec.ts
Normal file
718
packages/router/test/integration/navigation.spec.ts
Normal file
|
|
@ -0,0 +1,718 @@
|
|||
/**
|
||||
* @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 {Component, inject as coreInject, NgModule} from '@angular/core';
|
||||
import {Location} from '@angular/common';
|
||||
import {fakeAsync, TestBed, tick, inject} from '@angular/core/testing';
|
||||
import {
|
||||
provideRouter,
|
||||
Navigation,
|
||||
withRouterConfig,
|
||||
Router,
|
||||
NavigationStart,
|
||||
NavigationEnd,
|
||||
RouterLink,
|
||||
ActivatedRoute,
|
||||
Params,
|
||||
RouterModule,
|
||||
} from '../../src';
|
||||
import {
|
||||
RootCmp,
|
||||
SimpleCmp,
|
||||
onlyNavigationStartAndEnd,
|
||||
expectEvents,
|
||||
RelativeLinkCmp,
|
||||
createRoot,
|
||||
advance,
|
||||
} from './integration_helpers';
|
||||
|
||||
export function navigationIntegrationTestSuite() {
|
||||
describe('navigation', () => {
|
||||
it('should navigate to the current URL', fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([], withRouterConfig({onSameUrlNavigation: 'reload'}))],
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
router.resetConfig([
|
||||
{path: '', component: SimpleCmp},
|
||||
{path: 'simple', component: SimpleCmp},
|
||||
]);
|
||||
|
||||
const events: (NavigationStart | NavigationEnd)[] = [];
|
||||
router.events.subscribe((e) => onlyNavigationStartAndEnd(e) && events.push(e));
|
||||
|
||||
router.navigateByUrl('/simple');
|
||||
tick();
|
||||
|
||||
router.navigateByUrl('/simple');
|
||||
tick();
|
||||
|
||||
expectEvents(events, [
|
||||
[NavigationStart, '/simple'],
|
||||
[NavigationEnd, '/simple'],
|
||||
[NavigationStart, '/simple'],
|
||||
[NavigationEnd, '/simple'],
|
||||
]);
|
||||
}));
|
||||
|
||||
it('should override default onSameUrlNavigation with extras', async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([], withRouterConfig({onSameUrlNavigation: 'ignore'}))],
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
router.resetConfig([
|
||||
{path: '', component: SimpleCmp},
|
||||
{path: 'simple', component: SimpleCmp},
|
||||
]);
|
||||
|
||||
const events: (NavigationStart | NavigationEnd)[] = [];
|
||||
router.events.subscribe((e) => onlyNavigationStartAndEnd(e) && events.push(e));
|
||||
|
||||
await router.navigateByUrl('/simple');
|
||||
await router.navigateByUrl('/simple');
|
||||
// By default, the second navigation is ignored
|
||||
expectEvents(events, [
|
||||
[NavigationStart, '/simple'],
|
||||
[NavigationEnd, '/simple'],
|
||||
]);
|
||||
await router.navigateByUrl('/simple', {onSameUrlNavigation: 'reload'});
|
||||
// We overrode the `onSameUrlNavigation` value. This navigation should be processed.
|
||||
expectEvents(events, [
|
||||
[NavigationStart, '/simple'],
|
||||
[NavigationEnd, '/simple'],
|
||||
[NavigationStart, '/simple'],
|
||||
[NavigationEnd, '/simple'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should override default onSameUrlNavigation with extras', async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([], withRouterConfig({onSameUrlNavigation: 'reload'}))],
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
router.resetConfig([
|
||||
{path: '', component: SimpleCmp},
|
||||
{path: 'simple', component: SimpleCmp},
|
||||
]);
|
||||
|
||||
const events: (NavigationStart | NavigationEnd)[] = [];
|
||||
router.events.subscribe((e) => onlyNavigationStartAndEnd(e) && events.push(e));
|
||||
|
||||
await router.navigateByUrl('/simple');
|
||||
await router.navigateByUrl('/simple');
|
||||
expectEvents(events, [
|
||||
[NavigationStart, '/simple'],
|
||||
[NavigationEnd, '/simple'],
|
||||
[NavigationStart, '/simple'],
|
||||
[NavigationEnd, '/simple'],
|
||||
]);
|
||||
|
||||
events.length = 0;
|
||||
await router.navigateByUrl('/simple', {onSameUrlNavigation: 'ignore'});
|
||||
expectEvents(events, []);
|
||||
});
|
||||
|
||||
it('should set transient navigation info', async () => {
|
||||
let observedInfo: unknown;
|
||||
const router = TestBed.inject(Router);
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'simple',
|
||||
component: SimpleCmp,
|
||||
canActivate: [
|
||||
() => {
|
||||
observedInfo = coreInject(Router).getCurrentNavigation()?.extras?.info;
|
||||
return true;
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
await router.navigateByUrl('/simple', {info: 'navigation info'});
|
||||
expect(observedInfo).toEqual('navigation info');
|
||||
});
|
||||
|
||||
it('should set transient navigation info for routerlink', async () => {
|
||||
let observedInfo: unknown;
|
||||
const router = TestBed.inject(Router);
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'simple',
|
||||
component: SimpleCmp,
|
||||
canActivate: [
|
||||
() => {
|
||||
observedInfo = coreInject(Router).getCurrentNavigation()?.extras?.info;
|
||||
return true;
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
@Component({
|
||||
imports: [RouterLink],
|
||||
template: `<a #simpleLink [routerLink]="'/simple'" [info]="simpleLink"></a>`,
|
||||
})
|
||||
class App {}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.autoDetectChanges();
|
||||
const anchor = fixture.nativeElement.querySelector('a');
|
||||
anchor.click();
|
||||
await fixture.whenStable();
|
||||
|
||||
// An example use-case might be to pass the clicked link along with the navigation
|
||||
// information
|
||||
expect(observedInfo).toBeInstanceOf(HTMLAnchorElement);
|
||||
});
|
||||
|
||||
it('should make transient navigation info available in redirect', async () => {
|
||||
let observedInfo: unknown;
|
||||
const router = TestBed.inject(Router);
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'redirect',
|
||||
component: SimpleCmp,
|
||||
canActivate: [() => coreInject(Router).parseUrl('/simple')],
|
||||
},
|
||||
{
|
||||
path: 'simple',
|
||||
component: SimpleCmp,
|
||||
canActivate: [
|
||||
() => {
|
||||
observedInfo = coreInject(Router).getCurrentNavigation()?.extras?.info;
|
||||
return true;
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
await router.navigateByUrl('/redirect', {info: 'navigation info'});
|
||||
expect(observedInfo).toBe('navigation info');
|
||||
expect(router.url).toEqual('/simple');
|
||||
});
|
||||
|
||||
it('should ignore empty paths in relative links', fakeAsync(
|
||||
inject([Router], (router: Router) => {
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'foo',
|
||||
children: [{path: 'bar', children: [{path: '', component: RelativeLinkCmp}]}],
|
||||
},
|
||||
]);
|
||||
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.navigateByUrl('/foo/bar');
|
||||
advance(fixture);
|
||||
|
||||
const link = fixture.nativeElement.querySelector('a');
|
||||
expect(link.getAttribute('href')).toEqual('/foo/simple');
|
||||
}),
|
||||
));
|
||||
|
||||
it('should set the restoredState to null when executing imperative navigations', fakeAsync(
|
||||
inject([Router], (router: Router) => {
|
||||
router.resetConfig([
|
||||
{path: '', component: SimpleCmp},
|
||||
{path: 'simple', component: SimpleCmp},
|
||||
]);
|
||||
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
let event: NavigationStart;
|
||||
router.events.subscribe((e) => {
|
||||
if (e instanceof NavigationStart) {
|
||||
event = e;
|
||||
}
|
||||
});
|
||||
|
||||
router.navigateByUrl('/simple');
|
||||
tick();
|
||||
|
||||
expect(event!.navigationTrigger).toEqual('imperative');
|
||||
expect(event!.restoredState).toEqual(null);
|
||||
}),
|
||||
));
|
||||
|
||||
it('should set history.state if passed using imperative navigation', fakeAsync(
|
||||
inject([Router, Location], (router: Router, location: Location) => {
|
||||
router.resetConfig([
|
||||
{path: '', component: SimpleCmp},
|
||||
{path: 'simple', component: SimpleCmp},
|
||||
]);
|
||||
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
let navigation: Navigation = null!;
|
||||
router.events.subscribe((e) => {
|
||||
if (e instanceof NavigationStart) {
|
||||
navigation = router.getCurrentNavigation()!;
|
||||
}
|
||||
});
|
||||
|
||||
router.navigateByUrl('/simple', {state: {foo: 'bar'}});
|
||||
tick();
|
||||
|
||||
const state = location.getState() as any;
|
||||
expect(state.foo).toBe('bar');
|
||||
expect(state).toEqual({foo: 'bar', navigationId: 2});
|
||||
expect(navigation.extras.state).toBeDefined();
|
||||
expect(navigation.extras.state).toEqual({foo: 'bar'});
|
||||
}),
|
||||
));
|
||||
|
||||
it('should set history.state when navigation with browser back and forward', fakeAsync(
|
||||
inject([Router, Location], (router: Router, location: Location) => {
|
||||
router.resetConfig([
|
||||
{path: '', component: SimpleCmp},
|
||||
{path: 'simple', component: SimpleCmp},
|
||||
]);
|
||||
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
let navigation: Navigation = null!;
|
||||
router.events.subscribe((e) => {
|
||||
if (e instanceof NavigationStart) {
|
||||
navigation = <Navigation>router.getCurrentNavigation()!;
|
||||
}
|
||||
});
|
||||
|
||||
let state: Record<string, string> = {foo: 'bar'};
|
||||
router.navigateByUrl('/simple', {state});
|
||||
tick();
|
||||
location.back();
|
||||
tick();
|
||||
location.forward();
|
||||
tick();
|
||||
|
||||
expect(navigation.extras.state).toBeDefined();
|
||||
expect(navigation.extras.state).toEqual(state);
|
||||
|
||||
// Manually set state rather than using navigate()
|
||||
state = {bar: 'foo'};
|
||||
location.replaceState(location.path(), '', state);
|
||||
location.back();
|
||||
tick();
|
||||
location.forward();
|
||||
tick();
|
||||
|
||||
expect(navigation.extras.state).toBeDefined();
|
||||
expect(navigation.extras.state).toEqual(state);
|
||||
}),
|
||||
));
|
||||
|
||||
it('should navigate correctly when using `Location#historyGo', fakeAsync(
|
||||
inject([Router, Location], (router: Router, location: Location) => {
|
||||
router.resetConfig([
|
||||
{path: 'first', component: SimpleCmp},
|
||||
{path: 'second', component: SimpleCmp},
|
||||
]);
|
||||
|
||||
createRoot(router, RootCmp);
|
||||
|
||||
router.navigateByUrl('/first');
|
||||
tick();
|
||||
router.navigateByUrl('/second');
|
||||
tick();
|
||||
expect(router.url).toEqual('/second');
|
||||
|
||||
location.historyGo(-1);
|
||||
tick();
|
||||
expect(router.url).toEqual('/first');
|
||||
|
||||
location.historyGo(1);
|
||||
tick();
|
||||
expect(router.url).toEqual('/second');
|
||||
|
||||
location.historyGo(-100);
|
||||
tick();
|
||||
expect(router.url).toEqual('/second');
|
||||
|
||||
location.historyGo(100);
|
||||
tick();
|
||||
expect(router.url).toEqual('/second');
|
||||
|
||||
location.historyGo(0);
|
||||
tick();
|
||||
expect(router.url).toEqual('/second');
|
||||
|
||||
location.historyGo();
|
||||
tick();
|
||||
expect(router.url).toEqual('/second');
|
||||
}),
|
||||
));
|
||||
|
||||
it('should not error if state is not {[key: string]: any}', fakeAsync(
|
||||
inject([Router, Location], (router: Router, location: Location) => {
|
||||
router.resetConfig([
|
||||
{path: '', component: SimpleCmp},
|
||||
{path: 'simple', component: SimpleCmp},
|
||||
]);
|
||||
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
let navigation: Navigation = null!;
|
||||
router.events.subscribe((e) => {
|
||||
if (e instanceof NavigationStart) {
|
||||
navigation = <Navigation>router.getCurrentNavigation()!;
|
||||
}
|
||||
});
|
||||
|
||||
location.replaceState('', '', 42);
|
||||
router.navigateByUrl('/simple');
|
||||
tick();
|
||||
location.back();
|
||||
advance(fixture);
|
||||
|
||||
// Angular does not support restoring state to the primitive.
|
||||
expect(navigation.extras.state).toEqual(undefined);
|
||||
expect(location.getState()).toEqual({navigationId: 3});
|
||||
}),
|
||||
));
|
||||
|
||||
it('should not pollute browser history when replaceUrl is set to true', fakeAsync(
|
||||
inject([Router, Location], (router: Router, location: Location) => {
|
||||
router.resetConfig([
|
||||
{path: '', component: SimpleCmp},
|
||||
{path: 'a', component: SimpleCmp},
|
||||
{path: 'b', component: SimpleCmp},
|
||||
]);
|
||||
|
||||
createRoot(router, RootCmp);
|
||||
|
||||
const replaceSpy = spyOn(location, 'replaceState');
|
||||
router.navigateByUrl('/a', {replaceUrl: true});
|
||||
router.navigateByUrl('/b', {replaceUrl: true});
|
||||
tick();
|
||||
|
||||
expect(replaceSpy.calls.count()).toEqual(1);
|
||||
}),
|
||||
));
|
||||
|
||||
it('should skip navigation if another navigation is already scheduled', fakeAsync(
|
||||
inject([Router, Location], (router: Router, location: Location) => {
|
||||
router.resetConfig([
|
||||
{path: '', component: SimpleCmp},
|
||||
{path: 'a', component: SimpleCmp},
|
||||
{path: 'b', component: SimpleCmp},
|
||||
]);
|
||||
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.navigate(['/a'], {
|
||||
queryParams: {a: true},
|
||||
queryParamsHandling: 'merge',
|
||||
replaceUrl: true,
|
||||
});
|
||||
router.navigate(['/b'], {
|
||||
queryParams: {b: true},
|
||||
queryParamsHandling: 'merge',
|
||||
replaceUrl: true,
|
||||
});
|
||||
tick();
|
||||
|
||||
/**
|
||||
* Why do we have '/b?b=true' and not '/b?a=true&b=true'?
|
||||
*
|
||||
* This is because the router has the right to stop a navigation mid-flight if another
|
||||
* navigation has been already scheduled. This is why we can use a top-level guard
|
||||
* to perform redirects. Calling `navigate` in such a guard will stop the navigation, and
|
||||
* the components won't be instantiated.
|
||||
*
|
||||
* This is a fundamental property of the router: it only cares about its latest state.
|
||||
*
|
||||
* This means that components should only map params to something else, not reduce them.
|
||||
* In other words, the following component is asking for trouble:
|
||||
*
|
||||
* ```
|
||||
* class MyComponent {
|
||||
* constructor(a: ActivatedRoute) {
|
||||
* a.params.scan(...)
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* This also means "queryParamsHandling: 'merge'" should only be used to merge with
|
||||
* long-living query parameters (e.g., debug).
|
||||
*/
|
||||
expect(router.url).toEqual('/b?b=true');
|
||||
}),
|
||||
));
|
||||
});
|
||||
|
||||
describe('should execute navigations serially', () => {
|
||||
let log: Array<string | Params> = [];
|
||||
|
||||
beforeEach(() => {
|
||||
log = [];
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: 'trueRightAway',
|
||||
useValue: () => {
|
||||
log.push('trueRightAway');
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: 'trueIn2Seconds',
|
||||
useValue: () => {
|
||||
log.push('trueIn2Seconds-start');
|
||||
let res: (value: boolean) => void;
|
||||
const p = new Promise<boolean>((r) => (res = r));
|
||||
setTimeout(() => {
|
||||
log.push('trueIn2Seconds-end');
|
||||
res(true);
|
||||
}, 2000);
|
||||
return p;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('route activation', () => {
|
||||
@Component({
|
||||
template: '<router-outlet></router-outlet>',
|
||||
standalone: false,
|
||||
})
|
||||
class Parent {
|
||||
constructor(route: ActivatedRoute) {
|
||||
route.params.subscribe((s: Params) => {
|
||||
log.push(s);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<router-outlet (deactivate)="logDeactivate('primary')"></router-outlet>
|
||||
<router-outlet name="first" (deactivate)="logDeactivate('first')"></router-outlet>
|
||||
<router-outlet name="second" (deactivate)="logDeactivate('second')"></router-outlet>
|
||||
`,
|
||||
standalone: false,
|
||||
})
|
||||
class NamedOutletHost {
|
||||
logDeactivate(route: string) {
|
||||
log.push(route + ' deactivate');
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: 'child1',
|
||||
standalone: false,
|
||||
})
|
||||
class Child1 {
|
||||
constructor() {
|
||||
log.push('child1 constructor');
|
||||
}
|
||||
ngOnDestroy() {
|
||||
log.push('child1 destroy');
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: 'child2',
|
||||
standalone: false,
|
||||
})
|
||||
class Child2 {
|
||||
constructor() {
|
||||
log.push('child2 constructor');
|
||||
}
|
||||
ngOnDestroy() {
|
||||
log.push('child2 destroy');
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: 'child3',
|
||||
standalone: false,
|
||||
})
|
||||
class Child3 {
|
||||
constructor() {
|
||||
log.push('child3 constructor');
|
||||
}
|
||||
ngOnDestroy() {
|
||||
log.push('child3 destroy');
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [Parent, NamedOutletHost, Child1, Child2, Child3],
|
||||
imports: [RouterModule.forRoot([])],
|
||||
})
|
||||
class TestModule {}
|
||||
|
||||
it('should advance the parent route after deactivating its children', fakeAsync(() => {
|
||||
TestBed.configureTestingModule({imports: [TestModule]});
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'parent/:id',
|
||||
component: Parent,
|
||||
children: [
|
||||
{path: 'child1', component: Child1},
|
||||
{path: 'child2', component: Child2},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
router.navigateByUrl('/parent/1/child1');
|
||||
advance(fixture);
|
||||
|
||||
router.navigateByUrl('/parent/2/child2');
|
||||
advance(fixture);
|
||||
|
||||
expect(location.path()).toEqual('/parent/2/child2');
|
||||
expect(log).toEqual([
|
||||
{id: '1'},
|
||||
'child1 constructor',
|
||||
'child1 destroy',
|
||||
{id: '2'},
|
||||
'child2 constructor',
|
||||
]);
|
||||
}));
|
||||
|
||||
it('should deactivate outlet children with componentless parent', fakeAsync(() => {
|
||||
TestBed.configureTestingModule({imports: [TestModule]});
|
||||
const router = TestBed.inject(Router);
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.resetConfig([
|
||||
{
|
||||
path: 'named-outlets',
|
||||
component: NamedOutletHost,
|
||||
children: [
|
||||
{
|
||||
path: 'home',
|
||||
children: [
|
||||
{path: '', component: Child1, outlet: 'first'},
|
||||
{path: '', component: Child2, outlet: 'second'},
|
||||
{path: 'primary', component: Child3},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
children: [
|
||||
{path: '', component: Child1, outlet: 'first'},
|
||||
{path: '', component: Child2, outlet: 'second'},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'other',
|
||||
component: Parent,
|
||||
},
|
||||
]);
|
||||
|
||||
router.navigateByUrl('/named-outlets/home/primary');
|
||||
advance(fixture);
|
||||
expect(log).toEqual([
|
||||
'child3 constructor', // primary outlet always first
|
||||
'child1 constructor',
|
||||
'child2 constructor',
|
||||
]);
|
||||
log.length = 0;
|
||||
|
||||
router.navigateByUrl('/named-outlets/about');
|
||||
advance(fixture);
|
||||
expect(log).toEqual([
|
||||
'child3 destroy',
|
||||
'primary deactivate',
|
||||
'child1 destroy',
|
||||
'first deactivate',
|
||||
'child2 destroy',
|
||||
'second deactivate',
|
||||
'child1 constructor',
|
||||
'child2 constructor',
|
||||
]);
|
||||
log.length = 0;
|
||||
|
||||
router.navigateByUrl('/other');
|
||||
advance(fixture);
|
||||
expect(log).toEqual([
|
||||
'child1 destroy',
|
||||
'first deactivate',
|
||||
'child2 destroy',
|
||||
'second deactivate',
|
||||
// route param subscription from 'Parent' component
|
||||
{},
|
||||
]);
|
||||
}));
|
||||
|
||||
it('should work between aux outlets under two levels of empty path parents', fakeAsync(() => {
|
||||
TestBed.configureTestingModule({imports: [TestModule]});
|
||||
const router = TestBed.inject(Router);
|
||||
router.resetConfig([
|
||||
{
|
||||
path: '',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: NamedOutletHost,
|
||||
children: [
|
||||
{path: 'one', component: Child1, outlet: 'first'},
|
||||
{path: 'two', component: Child2, outlet: 'first'},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.navigateByUrl('/(first:one)');
|
||||
advance(fixture);
|
||||
expect(log).toEqual(['child1 constructor']);
|
||||
|
||||
log.length = 0;
|
||||
router.navigateByUrl('/(first:two)');
|
||||
advance(fixture);
|
||||
expect(log).toEqual(['child1 destroy', 'first deactivate', 'child2 constructor']);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not wait for prior navigations to start a new navigation', fakeAsync(
|
||||
inject([Router, Location], (router: Router) => {
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.resetConfig([
|
||||
{path: 'a', component: SimpleCmp, canActivate: ['trueRightAway', 'trueIn2Seconds']},
|
||||
{path: 'b', component: SimpleCmp, canActivate: ['trueRightAway', 'trueIn2Seconds']},
|
||||
]);
|
||||
|
||||
router.navigateByUrl('/a');
|
||||
tick(100);
|
||||
fixture.detectChanges();
|
||||
|
||||
router.navigateByUrl('/b');
|
||||
tick(100); // 200
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(log).toEqual([
|
||||
'trueRightAway',
|
||||
'trueIn2Seconds-start',
|
||||
'trueRightAway',
|
||||
'trueIn2Seconds-start',
|
||||
]);
|
||||
|
||||
tick(2000); // 2200
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(log).toEqual([
|
||||
'trueRightAway',
|
||||
'trueIn2Seconds-start',
|
||||
'trueRightAway',
|
||||
'trueIn2Seconds-start',
|
||||
'trueIn2Seconds-end',
|
||||
'trueIn2Seconds-end',
|
||||
]);
|
||||
}),
|
||||
));
|
||||
});
|
||||
}
|
||||
449
packages/router/test/integration/navigation_errors.spec.ts
Normal file
449
packages/router/test/integration/navigation_errors.spec.ts
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
/**
|
||||
* @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 {inject as coreInject, Injectable} from '@angular/core';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
import {Location} from '@angular/common';
|
||||
import {fakeAsync, TestBed, tick, inject} from '@angular/core/testing';
|
||||
import {
|
||||
Router,
|
||||
NavigationStart,
|
||||
NavigationError,
|
||||
RoutesRecognized,
|
||||
GuardsCheckStart,
|
||||
Event,
|
||||
ChildActivationStart,
|
||||
ActivationStart,
|
||||
GuardsCheckEnd,
|
||||
ResolveStart,
|
||||
ResolveEnd,
|
||||
ActivationEnd,
|
||||
ChildActivationEnd,
|
||||
NavigationEnd,
|
||||
provideRouter,
|
||||
withRouterConfig,
|
||||
withNavigationErrorHandler,
|
||||
RouterModule,
|
||||
RedirectCommand,
|
||||
NavigationCancel,
|
||||
NavigationCancellationCode,
|
||||
} from '../../src';
|
||||
import {RouterTestingHarness} from '@angular/router/testing';
|
||||
import {
|
||||
createRoot,
|
||||
RootCmp,
|
||||
BlankCmp,
|
||||
UserCmp,
|
||||
advance,
|
||||
expectEvents,
|
||||
SimpleCmp,
|
||||
ThrowingCmp,
|
||||
ConditionalThrowingCmp,
|
||||
EmptyQueryParamsCmp,
|
||||
} from './integration_helpers';
|
||||
|
||||
export function navigationErrorsIntegrationSuite() {
|
||||
it('should handle failed navigations gracefully', fakeAsync(
|
||||
inject([Router], (router: Router) => {
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.resetConfig([{path: 'user/:name', component: UserCmp}]);
|
||||
|
||||
const recordedEvents: Event[] = [];
|
||||
router.events.forEach((e) => recordedEvents.push(e));
|
||||
|
||||
let e: any;
|
||||
router.navigateByUrl('/invalid').catch((_) => (e = _));
|
||||
advance(fixture);
|
||||
expect(e.message).toContain('Cannot match any routes');
|
||||
|
||||
router.navigateByUrl('/user/fedor');
|
||||
advance(fixture);
|
||||
|
||||
expect(fixture.nativeElement).toHaveText('user fedor');
|
||||
|
||||
expectEvents(recordedEvents, [
|
||||
[NavigationStart, '/invalid'],
|
||||
[NavigationError, '/invalid'],
|
||||
|
||||
[NavigationStart, '/user/fedor'],
|
||||
[RoutesRecognized, '/user/fedor'],
|
||||
[GuardsCheckStart, '/user/fedor'],
|
||||
[ChildActivationStart],
|
||||
[ActivationStart],
|
||||
[GuardsCheckEnd, '/user/fedor'],
|
||||
[ResolveStart, '/user/fedor'],
|
||||
[ResolveEnd, '/user/fedor'],
|
||||
[ActivationEnd],
|
||||
[ChildActivationEnd],
|
||||
[NavigationEnd, '/user/fedor'],
|
||||
]);
|
||||
}),
|
||||
));
|
||||
|
||||
it('should be able to provide an error handler with DI dependencies', async () => {
|
||||
@Injectable({providedIn: 'root'})
|
||||
class Handler {
|
||||
handlerCalled = false;
|
||||
}
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideRouter(
|
||||
[
|
||||
{
|
||||
path: 'throw',
|
||||
canMatch: [
|
||||
() => {
|
||||
throw new Error('');
|
||||
},
|
||||
],
|
||||
component: BlankCmp,
|
||||
},
|
||||
],
|
||||
withRouterConfig({resolveNavigationPromiseOnError: true}),
|
||||
withNavigationErrorHandler(() => (coreInject(Handler).handlerCalled = true)),
|
||||
),
|
||||
],
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
await router.navigateByUrl('/throw');
|
||||
expect(TestBed.inject(Handler).handlerCalled).toBeTrue();
|
||||
});
|
||||
|
||||
it('can redirect from error handler with RouterModule.forRoot', async () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(
|
||||
[
|
||||
{
|
||||
path: 'throw',
|
||||
canMatch: [
|
||||
() => {
|
||||
throw new Error('');
|
||||
},
|
||||
],
|
||||
component: BlankCmp,
|
||||
},
|
||||
{path: 'error', component: BlankCmp},
|
||||
],
|
||||
{
|
||||
resolveNavigationPromiseOnError: true,
|
||||
errorHandler: () => new RedirectCommand(coreInject(Router).parseUrl('/error')),
|
||||
},
|
||||
),
|
||||
],
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
let emitNavigationError = false;
|
||||
let emitNavigationCancelWithRedirect = false;
|
||||
router.events.subscribe((e) => {
|
||||
if (e instanceof NavigationError) {
|
||||
emitNavigationError = true;
|
||||
}
|
||||
if (e instanceof NavigationCancel && e.code === NavigationCancellationCode.Redirect) {
|
||||
emitNavigationCancelWithRedirect = true;
|
||||
}
|
||||
});
|
||||
await router.navigateByUrl('/throw');
|
||||
expect(router.url).toEqual('/error');
|
||||
expect(emitNavigationError).toBe(false);
|
||||
expect(emitNavigationCancelWithRedirect).toBe(true);
|
||||
});
|
||||
|
||||
it('can redirect from error handler', async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideRouter(
|
||||
[
|
||||
{
|
||||
path: 'throw',
|
||||
canMatch: [
|
||||
() => {
|
||||
throw new Error('');
|
||||
},
|
||||
],
|
||||
component: BlankCmp,
|
||||
},
|
||||
{path: 'error', component: BlankCmp},
|
||||
],
|
||||
withRouterConfig({resolveNavigationPromiseOnError: true}),
|
||||
withNavigationErrorHandler(
|
||||
() => new RedirectCommand(coreInject(Router).parseUrl('/error')),
|
||||
),
|
||||
),
|
||||
],
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
let emitNavigationError = false;
|
||||
let emitNavigationCancelWithRedirect = false;
|
||||
router.events.subscribe((e) => {
|
||||
if (e instanceof NavigationError) {
|
||||
emitNavigationError = true;
|
||||
}
|
||||
if (e instanceof NavigationCancel && e.code === NavigationCancellationCode.Redirect) {
|
||||
emitNavigationCancelWithRedirect = true;
|
||||
}
|
||||
});
|
||||
await router.navigateByUrl('/throw');
|
||||
expect(router.url).toEqual('/error');
|
||||
expect(emitNavigationError).toBe(false);
|
||||
expect(emitNavigationCancelWithRedirect).toBe(true);
|
||||
});
|
||||
|
||||
it('should not break navigation if an error happens in NavigationErrorHandler', async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideRouter(
|
||||
[
|
||||
{
|
||||
path: 'throw',
|
||||
canMatch: [
|
||||
() => {
|
||||
throw new Error('');
|
||||
},
|
||||
],
|
||||
component: BlankCmp,
|
||||
},
|
||||
{path: '**', component: BlankCmp},
|
||||
],
|
||||
withRouterConfig({resolveNavigationPromiseOnError: true}),
|
||||
withNavigationErrorHandler(() => {
|
||||
throw new Error('e');
|
||||
}),
|
||||
),
|
||||
],
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
});
|
||||
|
||||
// Errors should behave the same for both deferred and eager URL update strategies
|
||||
(['deferred', 'eager'] as const).forEach((urlUpdateStrategy) => {
|
||||
it('should dispatch NavigationError after the url has been reset back', fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([], withRouterConfig({urlUpdateStrategy}))],
|
||||
});
|
||||
const router: Router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.resetConfig([
|
||||
{path: 'simple', component: SimpleCmp},
|
||||
{path: 'throwing', component: ThrowingCmp},
|
||||
]);
|
||||
|
||||
router.navigateByUrl('/simple');
|
||||
advance(fixture);
|
||||
|
||||
let routerUrlBeforeEmittingError = '';
|
||||
let locationUrlBeforeEmittingError = '';
|
||||
router.events.forEach((e) => {
|
||||
if (e instanceof NavigationError) {
|
||||
routerUrlBeforeEmittingError = router.url;
|
||||
locationUrlBeforeEmittingError = location.path();
|
||||
}
|
||||
});
|
||||
router.navigateByUrl('/throwing').catch(() => null);
|
||||
advance(fixture);
|
||||
|
||||
expect(routerUrlBeforeEmittingError).toEqual('/simple');
|
||||
expect(locationUrlBeforeEmittingError).toEqual('/simple');
|
||||
}));
|
||||
|
||||
it('can renavigate to throwing component', fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))],
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
router.resetConfig([
|
||||
{path: '', component: BlankCmp},
|
||||
{path: 'throwing', component: ConditionalThrowingCmp},
|
||||
]);
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
// Try navigating to a component which throws an error during activation.
|
||||
ConditionalThrowingCmp.throwError = true;
|
||||
expect(() => {
|
||||
router.navigateByUrl('/throwing');
|
||||
advance(fixture);
|
||||
}).toThrow();
|
||||
expect(location.path()).toEqual('');
|
||||
expect(fixture.nativeElement.innerHTML).not.toContain('throwing');
|
||||
|
||||
// Ensure we can re-navigate to that same URL and succeed.
|
||||
ConditionalThrowingCmp.throwError = false;
|
||||
router.navigateByUrl('/throwing');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/throwing');
|
||||
expect(fixture.nativeElement.innerHTML).toContain('throwing');
|
||||
}));
|
||||
|
||||
it('should reset the url with the right state when navigation errors', fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([], withRouterConfig({urlUpdateStrategy}))],
|
||||
});
|
||||
const router: Router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.resetConfig([
|
||||
{path: 'simple1', component: SimpleCmp},
|
||||
{path: 'simple2', component: SimpleCmp},
|
||||
{path: 'throwing', component: ThrowingCmp},
|
||||
]);
|
||||
|
||||
let event: NavigationStart;
|
||||
router.events.subscribe((e) => {
|
||||
if (e instanceof NavigationStart) {
|
||||
event = e;
|
||||
}
|
||||
});
|
||||
|
||||
router.navigateByUrl('/simple1');
|
||||
advance(fixture);
|
||||
const simple1NavStart = event!;
|
||||
|
||||
router.navigateByUrl('/throwing').catch(() => null);
|
||||
advance(fixture);
|
||||
|
||||
router.navigateByUrl('/simple2');
|
||||
advance(fixture);
|
||||
|
||||
location.back();
|
||||
tick();
|
||||
|
||||
expect(event!.restoredState!.navigationId).toEqual(simple1NavStart.id);
|
||||
}));
|
||||
|
||||
it('should not trigger another navigation when resetting the url back due to a NavigationError', fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideRouter([], withRouterConfig({urlUpdateStrategy}))],
|
||||
});
|
||||
const router = TestBed.inject(Router);
|
||||
router.onSameUrlNavigation = 'reload';
|
||||
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.resetConfig([
|
||||
{path: 'simple', component: SimpleCmp},
|
||||
{path: 'throwing', component: ThrowingCmp},
|
||||
]);
|
||||
|
||||
const events: any[] = [];
|
||||
router.events.forEach((e: any) => {
|
||||
if (e instanceof NavigationStart) {
|
||||
events.push(e.url);
|
||||
}
|
||||
});
|
||||
|
||||
router.navigateByUrl('/simple');
|
||||
advance(fixture);
|
||||
|
||||
router.navigateByUrl('/throwing').catch(() => null);
|
||||
advance(fixture);
|
||||
|
||||
// we do not trigger another navigation to /simple
|
||||
expect(events).toEqual(['/simple', '/throwing']);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should dispatch NavigationCancel after the url has been reset back', fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{provide: 'returnsFalse', useValue: () => false}],
|
||||
});
|
||||
|
||||
const router: Router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location);
|
||||
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.resetConfig([
|
||||
{path: 'simple', component: SimpleCmp},
|
||||
{
|
||||
path: 'throwing',
|
||||
loadChildren: jasmine.createSpy('doesnotmatter'),
|
||||
canLoad: ['returnsFalse'],
|
||||
},
|
||||
]);
|
||||
|
||||
router.navigateByUrl('/simple');
|
||||
advance(fixture);
|
||||
|
||||
let routerUrlBeforeEmittingError = '';
|
||||
let locationUrlBeforeEmittingError = '';
|
||||
router.events.forEach((e) => {
|
||||
if (e instanceof NavigationCancel) {
|
||||
expect(e.code).toBe(NavigationCancellationCode.GuardRejected);
|
||||
routerUrlBeforeEmittingError = router.url;
|
||||
locationUrlBeforeEmittingError = location.path();
|
||||
}
|
||||
});
|
||||
|
||||
location.go('/throwing');
|
||||
location.historyGo(0);
|
||||
advance(fixture);
|
||||
|
||||
expect(routerUrlBeforeEmittingError).toEqual('/simple');
|
||||
expect(locationUrlBeforeEmittingError).toEqual('/simple');
|
||||
}));
|
||||
|
||||
it('should recover from malformed uri errors', fakeAsync(
|
||||
inject([Router, Location], (router: Router, location: Location) => {
|
||||
router.resetConfig([{path: 'simple', component: SimpleCmp}]);
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
router.navigateByUrl('/invalid/url%with%percent');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('');
|
||||
}),
|
||||
));
|
||||
|
||||
it('should not swallow errors', fakeAsync(
|
||||
inject([Router], (router: Router) => {
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.resetConfig([{path: 'simple', component: SimpleCmp}]);
|
||||
|
||||
router.navigateByUrl('/invalid');
|
||||
expect(() => advance(fixture)).toThrow();
|
||||
|
||||
router.navigateByUrl('/invalid2');
|
||||
expect(() => advance(fixture)).toThrow();
|
||||
}),
|
||||
));
|
||||
|
||||
it('should not swallow errors from browser state update', async () => {
|
||||
const routerEvents: Event[] = [];
|
||||
TestBed.inject(Router).resetConfig([{path: '**', component: BlankCmp}]);
|
||||
TestBed.inject(Router).events.subscribe((e) => {
|
||||
routerEvents.push(e);
|
||||
});
|
||||
spyOn(TestBed.inject(Location), 'go').and.callFake(() => {
|
||||
throw new Error();
|
||||
});
|
||||
try {
|
||||
await RouterTestingHarness.create('/abc123');
|
||||
} catch {}
|
||||
// Ensure the first event is the start and that we get to the ResolveEnd event. If this is not
|
||||
// true, then NavigationError may have been triggered at a time we don't expect here.
|
||||
expect(routerEvents[0]).toBeInstanceOf(NavigationStart);
|
||||
expect(routerEvents[routerEvents.length - 2]).toBeInstanceOf(ResolveEnd);
|
||||
|
||||
expect(routerEvents[routerEvents.length - 1]).toBeInstanceOf(NavigationError);
|
||||
});
|
||||
|
||||
it('should throw an error when one of the commands is null/undefined', fakeAsync(
|
||||
inject([Router], (router: Router) => {
|
||||
createRoot(router, RootCmp);
|
||||
|
||||
router.resetConfig([{path: 'query', component: EmptyQueryParamsCmp}]);
|
||||
|
||||
expect(() => router.navigate([undefined, 'query'])).toThrowError(
|
||||
/The requested path contains undefined segment at index 0/,
|
||||
);
|
||||
}),
|
||||
));
|
||||
}
|
||||
Loading…
Reference in a new issue