Skip to content

Commit 15c4811

Browse files
atscottthePunderWoman
authored andcommitted
refactor(router): Update integration tests to cover navigation and history API (#53799)
This commit updates the router integration tests to cover both the classic History and the new Navigation API. There is more work to be done here, but this commit works to prove the efficacy of the `FakeNavigation` implementation. PR Close #53799
1 parent 7b4d275 commit 15c4811

14 files changed

+6396
-5900
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@
7878
"@types/chrome": "^0.0.258",
7979
"@types/convert-source-map": "^2.0.0",
8080
"@types/diff": "^5.0.0",
81-
"@types/dom-navigation": "^1.0.2",
8281
"@types/dom-view-transitions": "^1.0.1",
8382
"@types/hammerjs": "2.0.45",
8483
"@types/jasmine": "^5.0.0",

packages/common/BUILD.bazel

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ ng_module(
3030
),
3131
deps = [
3232
"//packages/core",
33-
"@npm//@types/dom-navigation",
3433
"@npm//rxjs",
3534
],
3635
)
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export interface NavigationEventMap {
10+
navigate: NavigateEvent;
11+
navigatesuccess: Event;
12+
navigateerror: ErrorEvent;
13+
currententrychange: NavigationCurrentEntryChangeEvent;
14+
}
15+
16+
export interface NavigationResult {
17+
committed: Promise<NavigationHistoryEntry>;
18+
finished: Promise<NavigationHistoryEntry>;
19+
}
20+
21+
export declare class Navigation extends EventTarget {
22+
entries(): NavigationHistoryEntry[];
23+
readonly currentEntry: NavigationHistoryEntry|null;
24+
updateCurrentEntry(options: NavigationUpdateCurrentEntryOptions): void;
25+
readonly transition: NavigationTransition|null;
26+
27+
readonly canGoBack: boolean;
28+
readonly canGoForward: boolean;
29+
30+
navigate(url: string, options?: NavigationNavigateOptions): NavigationResult;
31+
reload(options?: NavigationReloadOptions): NavigationResult;
32+
33+
traverseTo(key: string, options?: NavigationOptions): NavigationResult;
34+
back(options?: NavigationOptions): NavigationResult;
35+
forward(options?: NavigationOptions): NavigationResult;
36+
37+
onnavigate: ((this: Navigation, ev: NavigateEvent) => any)|null;
38+
onnavigatesuccess: ((this: Navigation, ev: Event) => any)|null;
39+
onnavigateerror: ((this: Navigation, ev: ErrorEvent) => any)|null;
40+
oncurrententrychange: ((this: Navigation, ev: NavigationCurrentEntryChangeEvent) => any)|null;
41+
42+
addEventListener<K extends keyof NavigationEventMap>(
43+
type: K,
44+
listener: (this: Navigation, ev: NavigationEventMap[K]) => any,
45+
options?: boolean|AddEventListenerOptions,
46+
): void;
47+
addEventListener(
48+
type: string,
49+
listener: EventListenerOrEventListenerObject,
50+
options?: boolean|AddEventListenerOptions,
51+
): void;
52+
removeEventListener<K extends keyof NavigationEventMap>(
53+
type: K,
54+
listener: (this: Navigation, ev: NavigationEventMap[K]) => any,
55+
options?: boolean|EventListenerOptions,
56+
): void;
57+
removeEventListener(
58+
type: string,
59+
listener: EventListenerOrEventListenerObject,
60+
options?: boolean|EventListenerOptions,
61+
): void;
62+
}
63+
64+
export declare class NavigationTransition {
65+
readonly navigationType: NavigationTypeString;
66+
readonly from: NavigationHistoryEntry;
67+
readonly finished: Promise<void>;
68+
}
69+
70+
export interface NavigationHistoryEntryEventMap {
71+
dispose: Event;
72+
}
73+
74+
export declare class NavigationHistoryEntry extends EventTarget {
75+
readonly key: string;
76+
readonly id: string;
77+
readonly url: string|null;
78+
readonly index: number;
79+
readonly sameDocument: boolean;
80+
81+
getState(): unknown;
82+
83+
ondispose: ((this: NavigationHistoryEntry, ev: Event) => any)|null;
84+
85+
addEventListener<K extends keyof NavigationHistoryEntryEventMap>(
86+
type: K,
87+
listener: (this: NavigationHistoryEntry, ev: NavigationHistoryEntryEventMap[K]) => any,
88+
options?: boolean|AddEventListenerOptions,
89+
): void;
90+
addEventListener(
91+
type: string,
92+
listener: EventListenerOrEventListenerObject,
93+
options?: boolean|AddEventListenerOptions,
94+
): void;
95+
removeEventListener<K extends keyof NavigationHistoryEntryEventMap>(
96+
type: K,
97+
listener: (this: NavigationHistoryEntry, ev: NavigationHistoryEntryEventMap[K]) => any,
98+
options?: boolean|EventListenerOptions,
99+
): void;
100+
removeEventListener(
101+
type: string,
102+
listener: EventListenerOrEventListenerObject,
103+
options?: boolean|EventListenerOptions,
104+
): void;
105+
}
106+
107+
type NavigationTypeString = 'reload'|'push'|'replace'|'traverse';
108+
109+
export interface NavigationUpdateCurrentEntryOptions {
110+
state: unknown;
111+
}
112+
113+
export interface NavigationOptions {
114+
info?: unknown;
115+
}
116+
117+
export interface NavigationNavigateOptions extends NavigationOptions {
118+
state?: unknown;
119+
history?: 'auto'|'push'|'replace';
120+
}
121+
122+
export interface NavigationReloadOptions extends NavigationOptions {
123+
state?: unknown;
124+
}
125+
126+
export declare class NavigationCurrentEntryChangeEvent extends Event {
127+
constructor(type: string, eventInit?: NavigationCurrentEntryChangeEventInit);
128+
129+
readonly navigationType: NavigationTypeString|null;
130+
readonly from: NavigationHistoryEntry;
131+
}
132+
133+
export interface NavigationCurrentEntryChangeEventInit extends EventInit {
134+
navigationType?: NavigationTypeString|null;
135+
from: NavigationHistoryEntry;
136+
}
137+
138+
export declare class NavigateEvent extends Event {
139+
constructor(type: string, eventInit?: NavigateEventInit);
140+
141+
readonly navigationType: NavigationTypeString;
142+
readonly canIntercept: boolean;
143+
readonly userInitiated: boolean;
144+
readonly hashChange: boolean;
145+
readonly destination: NavigationDestination;
146+
readonly signal: AbortSignal;
147+
readonly formData: FormData|null;
148+
readonly downloadRequest: string|null;
149+
readonly info?: unknown;
150+
151+
intercept(options?: NavigationInterceptOptions): void;
152+
scroll(): void;
153+
}
154+
155+
export interface NavigateEventInit extends EventInit {
156+
navigationType?: NavigationTypeString;
157+
canIntercept?: boolean;
158+
userInitiated?: boolean;
159+
hashChange?: boolean;
160+
destination: NavigationDestination;
161+
signal: AbortSignal;
162+
formData?: FormData|null;
163+
downloadRequest?: string|null;
164+
info?: unknown;
165+
}
166+
167+
export interface NavigationInterceptOptions {
168+
handler?: () => Promise<void>;
169+
focusReset?: 'after-transition'|'manual';
170+
scroll?: 'after-transition'|'manual';
171+
}
172+
173+
export declare class NavigationDestination {
174+
readonly url: string;
175+
readonly key: string|null;
176+
readonly id: string|null;
177+
readonly index: number;
178+
readonly sameDocument: boolean;
179+
180+
getState(): unknown;
181+
}

packages/common/src/navigation/platform_navigation.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88

99
import {Injectable} from '@angular/core';
1010

11+
import {NavigateEvent, Navigation, NavigationCurrentEntryChangeEvent, NavigationHistoryEntry, NavigationNavigateOptions, NavigationOptions, NavigationReloadOptions, NavigationResult, NavigationTransition, NavigationUpdateCurrentEntryOptions} from './navigation_types';
12+
1113
/**
1214
* This class wraps the platform Navigation API which allows server-specific and test
1315
* implementations.
1416
*/
15-
@Injectable({providedIn: 'platform', useFactory: () => window.navigation})
17+
@Injectable({providedIn: 'platform', useFactory: () => (window as any).navigation})
1618
export abstract class PlatformNavigation implements Navigation {
1719
abstract entries(): NavigationHistoryEntry[];
1820
abstract currentEntry: NavigationHistoryEntry|null;

packages/common/src/private_export.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
*/
88

99
export {DomAdapter as ɵDomAdapter, getDOM as ɵgetDOM, setRootDomAdapter as ɵsetRootDomAdapter} from './dom_adapter';
10+
export {PlatformNavigation as ɵPlatformNavigation} from './navigation/platform_navigation';

packages/common/testing/src/mock_platform_location.ts

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {LocationChangeEvent, LocationChangeListener, PlatformLocation} from '@angular/common';
10-
import {Inject, Injectable, InjectionToken, Optional} from '@angular/core';
9+
import {DOCUMENT, LocationChangeEvent, LocationChangeListener, PlatformLocation, ɵPlatformNavigation as PlatformNavigation} from '@angular/common';
10+
import {Inject, inject, Injectable, InjectionToken, Optional} from '@angular/core';
1111
import {Subject} from 'rxjs';
1212

13+
import {FakeNavigation} from './navigation/fake_navigation';
14+
1315
/**
1416
* Parser from https://tools.ietf.org/html/rfc3986#appendix-B
1517
* ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
@@ -250,3 +252,82 @@ export class MockPlatformLocation implements PlatformLocation {
250252
}
251253
}
252254
}
255+
256+
/**
257+
* Mock implementation of URL state.
258+
*/
259+
@Injectable()
260+
export class FakeNavigationPlatformLocation implements PlatformLocation {
261+
private _platformNavigation = inject(PlatformNavigation) as FakeNavigation;
262+
private window = inject(DOCUMENT).defaultView!;
263+
264+
constructor() {
265+
if (!(this._platformNavigation instanceof FakeNavigation)) {
266+
throw new Error(
267+
'FakePlatformNavigation cannot be used without FakeNavigation. Use ' +
268+
'`provideFakeNavigation` to have all these services provided together.',
269+
);
270+
}
271+
}
272+
273+
private config = inject(MOCK_PLATFORM_LOCATION_CONFIG, {optional: true});
274+
getBaseHrefFromDOM(): string {
275+
return this.config?.appBaseHref ?? '';
276+
}
277+
278+
onPopState(fn: LocationChangeListener): VoidFunction {
279+
this.window.addEventListener('popstate', fn);
280+
return () => this.window.removeEventListener('popstate', fn);
281+
}
282+
283+
onHashChange(fn: LocationChangeListener): VoidFunction {
284+
this.window.addEventListener('hashchange', fn as any);
285+
return () => this.window.removeEventListener('hashchange', fn as any);
286+
}
287+
288+
get href(): string {
289+
return this._platformNavigation.currentEntry.url!;
290+
}
291+
get protocol(): string {
292+
return new URL(this._platformNavigation.currentEntry.url!).protocol;
293+
}
294+
get hostname(): string {
295+
return new URL(this._platformNavigation.currentEntry.url!).hostname;
296+
}
297+
get port(): string {
298+
return new URL(this._platformNavigation.currentEntry.url!).port;
299+
}
300+
get pathname(): string {
301+
return new URL(this._platformNavigation.currentEntry.url!).pathname;
302+
}
303+
get search(): string {
304+
return new URL(this._platformNavigation.currentEntry.url!).search;
305+
}
306+
get hash(): string {
307+
return new URL(this._platformNavigation.currentEntry.url!).hash;
308+
}
309+
310+
pushState(state: any, title: string, url: string): void {
311+
this._platformNavigation.pushState(state, title, url);
312+
}
313+
314+
replaceState(state: any, title: string, url: string): void {
315+
this._platformNavigation.replaceState(state, title, url);
316+
}
317+
318+
forward(): void {
319+
this._platformNavigation.forward();
320+
}
321+
322+
back(): void {
323+
this._platformNavigation.back();
324+
}
325+
326+
historyGo(relativePosition: number = 0): void {
327+
this._platformNavigation.go(relativePosition);
328+
}
329+
330+
getState(): unknown {
331+
return this._platformNavigation.currentEntry.getHistoryState();
332+
}
333+
}

packages/common/testing/src/navigation/fake_navigation.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
// Prevents deletion of `Event` from `globalThis` during module loading.
10-
const Event = globalThis.Event;
9+
import {NavigateEvent, Navigation, NavigationCurrentEntryChangeEvent, NavigationDestination, NavigationHistoryEntry, NavigationInterceptOptions, NavigationNavigateOptions, NavigationOptions, NavigationReloadOptions, NavigationResult, NavigationTransition, NavigationTypeString, NavigationUpdateCurrentEntryOptions} from './navigation_types';
1110

1211
/**
1312
* Fake implementation of user agent history and navigation behavior. This is a

0 commit comments

Comments
 (0)