mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
296 lines
9.7 KiB
TypeScript
296 lines
9.7 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} 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,
|
|
} 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 new Promise((resolve) => setTimeout(resolve, 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();
|
|
});
|
|
});
|
|
});
|
|
|
|
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([]);
|
|
});
|
|
});
|
|
}
|