/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/router/test/integration/navigation_errors.spec.ts
440 строк
14 KB
SkyZeroZx
test(router): move timeout and autoTick helpers to shared testing utilities
10 фев 2026, 18:45
10 фев 2026, 18:45
0f47fda
Код
Авторство
О чём код?
/** * @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 {Location} from '@angular/common'; import {inject, Injectable} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {expect} from '@angular/private/testing/matchers'; import { ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationStart, provideRouter, RedirectCommand, ResolveEnd, ResolveStart, Router, RouterModule, RoutesRecognized, withNavigationErrorHandler, withRouterConfig, } from '../../src'; import {RouterTestingHarness} from '../../testing'; import { RootCmp, BlankCmp, UserCmp, expectEvents, SimpleCmp, ThrowingCmp, ConditionalThrowingCmp, EmptyQueryParamsCmp, createRoot, advance, simulateLocationChange, } from './integration_helpers'; import {timeout} from '@angular/private/testing'; export function navigationErrorsIntegrationSuite(browserAPI: 'history' | 'navigation') { it('should handle failed navigations gracefully', async () => { const router = TestBed.inject(Router); const fixture = await 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 = _)); await advance(fixture); expect(e.message).toContain('Cannot match any routes'); router.navigateByUrl('/user/fedor'); await 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(() => (inject(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(inject(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(inject(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 (${urlUpdateStrategy})`, async () => { TestBed.configureTestingModule({ providers: [provideRouter([], withRouterConfig({urlUpdateStrategy}))], }); const router = TestBed.inject(Router); const location = TestBed.inject(Location); const fixture = await createRoot(router, RootCmp); router.resetConfig([ {path: 'simple', component: SimpleCmp}, {path: 'throwing', component: ThrowingCmp}, ]); router.navigateByUrl('/simple'); await 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); await advance(fixture); expect(routerUrlBeforeEmittingError).toEqual('/simple'); expect(locationUrlBeforeEmittingError).toEqual('/simple'); }); it(`can renavigate to throwing component (${urlUpdateStrategy})`, async () => { 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 = await createRoot(router, RootCmp); // Try navigating to a component which throws an error during activation. ConditionalThrowingCmp.throwError = true; await expectAsync(router.navigateByUrl('/throwing')).toBeRejected(); 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'); await 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 (${urlUpdateStrategy})`, async () => { TestBed.configureTestingModule({ providers: [provideRouter([], withRouterConfig({urlUpdateStrategy}))], }); const router = TestBed.inject(Router); const location = TestBed.inject(Location); const fixture = await 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'); await advance(fixture); const simple1NavStart = event!; router.navigateByUrl('/throwing').catch(() => null); await advance(fixture); router.navigateByUrl('/simple2'); await advance(fixture); location.back(); await timeout(); expect(event!.restoredState!.navigationId).toEqual(simple1NavStart.id); }); it(`should not trigger another navigation when resetting the url back due to a NavigationError (${urlUpdateStrategy})`, async () => { TestBed.configureTestingModule({ providers: [provideRouter([], withRouterConfig({urlUpdateStrategy}))], }); const router = TestBed.inject(Router); router.onSameUrlNavigation = 'reload'; const fixture = await 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'); await advance(fixture); router.navigateByUrl('/throwing').catch(() => null); await 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', async () => { const router = TestBed.inject(Router); const location = TestBed.inject(Location); const fixture = await createRoot(router, RootCmp); router.resetConfig([ {path: 'simple', component: SimpleCmp}, { path: 'throwing', loadChildren: jasmine.createSpy('doesnotmatter'), canLoad: [() => false], }, ]); router.navigateByUrl('/simple'); await 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(); } }); simulateLocationChange('/throwing', browserAPI); await advance(fixture); expect(routerUrlBeforeEmittingError).toEqual('/simple'); expect(locationUrlBeforeEmittingError).toEqual('/simple'); }); it('should recover from malformed uri errors', async () => { const router = TestBed.inject(Router); const location = TestBed.inject(Location); router.resetConfig([{path: 'simple', component: SimpleCmp}]); const fixture = await createRoot(router, RootCmp); router.navigateByUrl('/invalid/url%with%percent'); await advance(fixture); expect(location.path()).toEqual(''); }); it('should not swallow errors', async () => { const router = TestBed.inject(Router); const fixture = await createRoot(router, RootCmp); router.resetConfig([{path: 'simple', component: SimpleCmp}]); await expectAsync(router.navigateByUrl('/invalid')).toBeRejected(); await expectAsync(router.navigateByUrl('/invalid2')).toBeRejected(); }); it('should not swallow errors from browser state update', async () => { if (browserAPI === 'navigation') { // Router interfaces with the browser APIs at different times. We cannot use the same test for this because the events will be different. return; } 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', async () => { const router = TestBed.inject(Router); await createRoot(router, RootCmp); router.resetConfig([{path: 'query', component: EmptyQueryParamsCmp}]); expect(() => router.navigate([undefined, 'query'])).toThrowError( /The requested path contains undefined segment at index 0/, ); }); }