/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/router/test/with_platform_navigation.spec.ts
452 строки
14 KB
SkyZeroZx
test(router): Update tests to use `currentNavigation()`
07 июл 2026, 00:01
07 июл 2026, 00:01
d963920
Код
Авторство
О чём код?
/** * @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 {TestBed} from '@angular/core/testing'; import { NavigationStart, provideRouter, Event, Router, UrlSerializer, DefaultUrlSerializer, UrlTree, Params, } from '../src'; import {withExperimentalPlatformNavigation, withRouterConfig} from '../src/provide_router'; import {withBody, useAutoTick, timeout} from '@angular/private/testing'; import { PlatformLocation, Location, PlatformNavigation, BrowserPlatformLocation, ɵPRECOMMIT_HANDLER_SUPPORTED as PRECOMMIT_HANDLER_SUPPORTED, } from '@angular/common'; import { ɵFakeNavigation as FakeNavigation, ɵFakeNavigationPlatformLocation as FakeNavigationPlatformLocation, provideLocationMocks, MOCK_PLATFORM_LOCATION_CONFIG, } from '@angular/common/testing'; import {inject} from '@angular/core'; /// <reference types="dom-navigation" /> function isFirefox() { const userAgent = navigator.userAgent.toLowerCase(); if (userAgent.indexOf('firefox') != -1) { return true; } return false; } describe('withPlatformNavigation feature', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [provideRouter([], withExperimentalPlatformNavigation())], }); }); it('provides FakeNavigation by default', () => { expect(TestBed.inject(PlatformNavigation)).toBeInstanceOf(FakeNavigation); }); it('provides FakeNavigationPlatformLocation by default', () => { expect(TestBed.inject(PlatformLocation)).toBeInstanceOf(FakeNavigationPlatformLocation); }); describe('ensures location information is synced with navigation', () => { let location: Location; let navigation: PlatformNavigation; beforeEach(() => { location = TestBed.inject(Location); navigation = TestBed.inject(PlatformNavigation); }); it('state changes via location are reflected in navigation', () => { location.go('/a', undefined, {someState: 'someValue'}); expect(navigation.currentEntry!.getState()).toEqual( jasmine.objectContaining({someState: 'someValue'}), ); }); it('state changes via navigation are reflected in location', () => { navigation.navigate('/b', {state: {otherState: 'otherValue'}}); expect(location.getState()).toEqual(jasmine.objectContaining({otherState: 'otherValue'})); }); it('onurlchange tracks changes from navigation API', async () => { let changed = false; location.onUrlChange(() => { changed = true; }); navigation.navigate('/c'); expect(changed).toBeTrue(); }); it('onurlchange is not synchronous if navigation commit is delayed', async () => { let changed = false; location.onUrlChange(() => { changed = true; }); navigation.addEventListener('navigate', (e: any) => { e.intercept({ precommitHandler: () => new Promise((resolve) => setTimeout(resolve)), }); }); location.go('/c'); expect(changed).toBeFalse(); await timeout(1); expect(changed).toBeTrue(); }); }); describe('NavigateEvent and NavigationTransition', () => { useAutoTick(); let router: Router; let navigation: PlatformNavigation; beforeEach(async () => { router = TestBed.inject(Router); router.initialNavigation(); navigation = TestBed.inject(PlatformNavigation); await navigation.transition?.finished; }); it('should keep non-router triggered navigation unfinished while waiting for guards', async () => { router.resetConfig([ { path: '**', canActivate: [ () => new Promise<boolean>((resolve) => setTimeout(() => resolve(true), 10)), ], children: [], }, ]); navigation.navigate('/somepath'); await timeout(5); expect(navigation.transition).not.toBeNull(); await timeout(10); expect(navigation.transition).toBeNull(); }); // Needs update to FakeNavigation to match recent spec changes it('aborts ongoing router transition if navigation is aborted', async () => { router.resetConfig([ { path: 'blocked', children: [], canActivate: [() => new Promise((r) => setTimeout(() => r(true), 50))], }, {path: '**', children: []}, ]); // set up navigation navigation.addEventListener( 'navigate', (e: any) => e.intercept({precommitHandler: () => new Promise((_, reject) => setTimeout(reject, 5))}), {once: true}, ); navigation.navigate('/blocked'); await timeout(); expect(navigation.transition).not.toBeNull(); expect(router.currentNavigation()).not.toBeNull(); // wait for the rejection of the one-off handler, which will cancel the router transition await timeout(10); expect(router.currentNavigation()).toBeNull(); // wait for "rollback" navigation which is resetting the state await timeout(); expect(navigation.transition).toBeNull(); }); it('retains the original traversal NavigateEvent', async () => { router.resetConfig([{path: '**', children: []}]); await router.navigateByUrl('/first'); await timeout(); const navigateEvents: NavigateEvent[] = []; navigation.addEventListener('navigate', (e: NavigateEvent) => navigateEvents.push(e)); await navigation.back().finished; expect(navigateEvents.length).toBe(1); expect(navigateEvents[0].navigationType).toBe('traverse'); }); it('retains a single NavigateEvent across redirects', async () => { const navigateEvents: NavigateEvent[] = []; navigation.addEventListener('navigate', (e: NavigateEvent) => navigateEvents.push(e)); router.resetConfig([ {path: 'first', canActivate: [() => inject(Router).parseUrl('/redirected')], children: []}, {path: '**', children: []}, ]); const navPromise = router.navigateByUrl('/first'); if (TestBed.inject(PRECOMMIT_HANDLER_SUPPORTED)) { router.events.subscribe((e) => { if (e instanceof NavigationStart) { expect(navigateEvents.length).toBe(1); } }); } await navPromise; expect(navigateEvents.length).toBe(1); }); }); describe('eager url update', () => { useAutoTick(); let router: Router; beforeEach(() => { TestBed.configureTestingModule({ providers: [ provideRouter( [{path: '**', children: []}], withExperimentalPlatformNavigation(), withRouterConfig({urlUpdateStrategy: 'eager'}), ), ], }); router = TestBed.inject(Router); }); it('should keep router triggered navigation unfinished while waiting for guards', async () => { router.resetConfig([ { path: '**', canActivate: [ () => new Promise<boolean>((resolve) => setTimeout(() => resolve(true), 10)), ], children: [], }, ]); router.navigateByUrl('/somepath'); await timeout(5); const navigation = TestBed.inject(PlatformNavigation); const {finished} = navigation.transition!; expect(navigation.transition).not.toBeNull(); await timeout(10); expect(navigation.transition).toBeNull(); await expectAsync(finished).toBeResolved(); }); }); class TrailingSlashNormalizingUrlSerializer extends DefaultUrlSerializer { override parse(url: string): UrlTree { if (url !== '/' && url.endsWith('/')) { url = url.slice(0, -1); } return super.parse(url); } override serialize(tree: UrlTree): string { let url = super.serialize(tree); if (url !== '/' && url.endsWith('/')) { url = url.slice(0, -1); } return url; } } class QueryParamSortingUrlSerializer extends DefaultUrlSerializer { override parse(url: string): UrlTree { const tree = super.parse(url); const sorted: Params = {}; for (const key of Object.keys(tree.queryParams).sort()) { sorted[key] = tree.queryParams[key]; } tree.queryParams = sorted; return tree; } override serialize(tree: UrlTree): string { const sorted: Params = {}; for (const key of Object.keys(tree.queryParams).sort()) { sorted[key] = tree.queryParams[key]; } const newTree = new UrlTree(tree.root, sorted, tree.fragment); return super.serialize(newTree); } } describe('URL comparison and extraction bugs', () => { useAutoTick(); let router: Router; let navigation: PlatformNavigation; it('should not trigger new navigation when traversing back to a URL with trailing slash mismatch (with custom serializer)', async () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ providers: [ {provide: UrlSerializer, useClass: TrailingSlashNormalizingUrlSerializer}, {provide: PRECOMMIT_HANDLER_SUPPORTED, useValue: false}, provideRouter( [ {path: 'foo', children: []}, {path: 'bar', children: []}, ], withExperimentalPlatformNavigation(), ), ], }); navigation = TestBed.inject(PlatformNavigation); navigation.navigate('/foo/'); await timeout(); navigation.navigate('/bar'); await timeout(); router = TestBed.inject(Router); router.initialNavigation(); await navigation.transition?.finished; const navigateSpy = spyOn(navigation, 'navigate').and.callThrough(); await navigation.back().finished; await timeout(); expect(navigateSpy).not.toHaveBeenCalled(); }); it('should not trigger new navigation when traversing back to a URL with query param order mismatch (with custom serializer)', async () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ providers: [ {provide: UrlSerializer, useClass: QueryParamSortingUrlSerializer}, {provide: PRECOMMIT_HANDLER_SUPPORTED, useValue: false}, provideRouter( [ {path: 'foo', children: []}, {path: 'bar', children: []}, ], withExperimentalPlatformNavigation(), ), ], }); navigation = TestBed.inject(PlatformNavigation); navigation.navigate('/foo?b=2&a=1'); await timeout(); navigation.navigate('/bar'); await timeout(); router = TestBed.inject(Router); router.initialNavigation(); await navigation.transition?.finished; const navigateSpy = spyOn(navigation, 'navigate').and.callThrough(); await navigation.back().finished; await timeout(); expect(navigateSpy).not.toHaveBeenCalled(); }); it('should not intercept navigations outside the app root', async () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ providers: [ { provide: MOCK_PLATFORM_LOCATION_CONFIG, useValue: { startUrl: 'http://localhost/my-app/', appBaseHref: '/my-app/', }, }, provideRouter([{path: '**', children: []}], withExperimentalPlatformNavigation()), ], }); navigation = TestBed.inject(PlatformNavigation); let interceptCalled = false; navigation.addEventListener('navigate', (e: any) => { const originalIntercept = e.intercept; e.intercept = function (...args: any[]) { interceptCalled = true; originalIntercept.apply(this, args); }; }); router = TestBed.inject(Router); router.initialNavigation(); await navigation.transition?.finished; interceptCalled = false; navigation.navigate('http://localhost/other-app/foo'); await timeout(); expect(interceptCalled).toBeFalse(); }); }); }); describe('configuration error', () => { it('throws an error mentioning SpyLocation and the location mocks', () => { TestBed.configureTestingModule({ providers: [provideRouter([], withExperimentalPlatformNavigation()), provideLocationMocks()], }); expect(() => TestBed.inject(Location)).toThrowError(/SpyLocation.*provideLocationMocks/); }); }); if (typeof window !== 'undefined' && 'navigation' in window && !isFirefox()) { describe('real platform navigation', () => { const navigation = window.navigation as Navigation; beforeEach(() => { TestBed.configureTestingModule({ providers: [ provideRouter([{path: '**', children: []}], withExperimentalPlatformNavigation()), {provide: PlatformLocation, useClass: BrowserPlatformLocation}, {provide: PlatformNavigation, useFactory: () => navigation}, ], }); }); let router: Router; beforeEach(async () => { router = TestBed.inject(Router); router.initialNavigation(); await new Promise((r) => setTimeout(r)); }); // This would cause tests to fail without the navigation API support with an error like: // "Tests were interrupted because the page navigated to <localhost>/somewhere. This can happen when clicking a link, submitting a form or interacting with window.location." it( 'should convert navigations from regular anchors to same-document router navigations', withBody('<a href="/somewhere">link</a>', async () => { document.querySelector('a')!.click(); await new Promise((r) => setTimeout(r)); expect(router.url).toBe('/somewhere'); }), ); it('should not convert reload events to SPA navigations', async () => { const navigation = TestBed.inject(PlatformNavigation); navigation.addEventListener( 'navigate', (e: NavigateEvent) => { e.intercept({ handler: () => new Promise((_, reject) => setTimeout(reject, 2)), }); }, {once: true}, ); const routerEvents: Event[] = []; router.events.subscribe((e) => routerEvents.push(e)); router.resetConfig([ { path: '**', children: [], }, ]); navigation.reload(); await timeout(3); expect(routerEvents).toEqual([]); }); }); }