Files
angular__angular/packages/router/test/with_platform_navigation.spec.ts
SkyZeroZx d9639201b3 test(router): Update tests to use currentNavigation()
Updates router integration tests to use the `currentNavigation()` method instead of deprecated `getCurrentNavigation`.

Also replaces direct `setTimeout` calls with the `timeout()` utility function.
2026-07-06 14:01:11 -07:00

453 lines
14 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 {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([]);
});
});
}