diff --git a/.ng-dev/format.mts b/.ng-dev/format.mts index 1adfc739153..bc19dce5e3d 100644 --- a/.ng-dev/format.mts +++ b/.ng-dev/format.mts @@ -20,6 +20,7 @@ export const format: FormatConfig = { 'packages/examples/**/*.{js,ts}', 'packages/misc/**/*.{js,ts}', 'packages/private/**/*.{js,ts}', + 'packages/router/**/*.{js,ts}', 'packages/service-worker/**/*.{js,ts}', 'packages/upgrade/**/*.{js,ts}', @@ -33,7 +34,7 @@ export const format: FormatConfig = { }, 'clang-format': { 'matchers': [ - //'**/*.{js,ts}', + '**/*.{js,ts}', // TODO: burn down format failures and remove aio and integration exceptions. '!aio/**', '!integration/**', @@ -70,6 +71,7 @@ export const format: FormatConfig = { '!packages/examples/**/*.{js,ts}', '!packages/misc/**/*.{js,ts}', '!packages/private/**/*.{js,ts}', + '!packages/router/**/*.{js,ts}', '!packages/service-worker/**/*.{js,ts}', '!packages/upgrade/**/*.{js,ts}', ], diff --git a/.prettierrc b/.prettierrc index d73829a9323..efabfd03d4a 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,6 +2,7 @@ "printWidth": 100, "tabWidth": 2, "tabs": false, + "embeddedLanguageFormatting": "off", "singleQuote": true, "semicolon": true, "quoteProps": "preserve", diff --git a/packages/router/src/apply_redirects.ts b/packages/router/src/apply_redirects.ts index 8fa70ca3988..b46a1f346a2 100644 --- a/packages/router/src/apply_redirects.ts +++ b/packages/router/src/apply_redirects.ts @@ -16,9 +16,8 @@ import {navigationCancelingError} from './navigation_canceling_error'; import {Params, PRIMARY_OUTLET} from './shared'; import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree'; - export class NoMatch { - public segmentGroup: UrlSegmentGroup|null; + public segmentGroup: UrlSegmentGroup | null; constructor(segmentGroup?: UrlSegmentGroup) { this.segmentGroup = segmentGroup || null; @@ -40,23 +39,30 @@ export function absoluteRedirect(newTree: UrlTree): Observable { } export function namedOutletsRedirect(redirectTo: string): Observable { - return throwError(new RuntimeError( + return throwError( + new RuntimeError( RuntimeErrorCode.NAMED_OUTLET_REDIRECT, (typeof ngDevMode === 'undefined' || ngDevMode) && - `Only absolute redirects can have named outlets. redirectTo: '${redirectTo}'`)); + `Only absolute redirects can have named outlets. redirectTo: '${redirectTo}'`, + ), + ); } export function canLoadFails(route: Route): Observable { - return throwError(navigationCancelingError( + return throwError( + navigationCancelingError( (typeof ngDevMode === 'undefined' || ngDevMode) && - `Cannot load children because the guard of the route "path: '${ - route.path}'" returned false`, - NavigationCancellationCode.GuardRejected)); + `Cannot load children because the guard of the route "path: '${route.path}'" returned false`, + NavigationCancellationCode.GuardRejected, + ), + ); } - export class ApplyRedirects { - constructor(private urlSerializer: UrlSerializer, private urlTree: UrlTree) {} + constructor( + private urlSerializer: UrlSerializer, + private urlTree: UrlTree, + ) {} lineralizeSegments(route: Route, urlTree: UrlTree): Observable { let res: UrlSegment[] = []; @@ -76,9 +82,16 @@ export class ApplyRedirects { } applyRedirectCommands( - segments: UrlSegment[], redirectTo: string, posParams: {[k: string]: UrlSegment}): UrlTree { + segments: UrlSegment[], + redirectTo: string, + posParams: {[k: string]: UrlSegment}, + ): UrlTree { const newTree = this.applyRedirectCreateUrlTree( - redirectTo, this.urlSerializer.parse(redirectTo), segments, posParams); + redirectTo, + this.urlSerializer.parse(redirectTo), + segments, + posParams, + ); if (redirectTo.startsWith('/')) { throw new AbsoluteRedirect(newTree); } @@ -86,12 +99,17 @@ export class ApplyRedirects { } applyRedirectCreateUrlTree( - redirectTo: string, urlTree: UrlTree, segments: UrlSegment[], - posParams: {[k: string]: UrlSegment}): UrlTree { + redirectTo: string, + urlTree: UrlTree, + segments: UrlSegment[], + posParams: {[k: string]: UrlSegment}, + ): UrlTree { const newRoot = this.createSegmentGroup(redirectTo, urlTree.root, segments, posParams); return new UrlTree( - newRoot, this.createQueryParams(urlTree.queryParams, this.urlTree.queryParams), - urlTree.fragment); + newRoot, + this.createQueryParams(urlTree.queryParams, this.urlTree.queryParams), + urlTree.fragment, + ); } createQueryParams(redirectToParams: Params, actualParams: Params): Params { @@ -109,8 +127,11 @@ export class ApplyRedirects { } createSegmentGroup( - redirectTo: string, group: UrlSegmentGroup, segments: UrlSegment[], - posParams: {[k: string]: UrlSegment}): UrlSegmentGroup { + redirectTo: string, + group: UrlSegmentGroup, + segments: UrlSegment[], + posParams: {[k: string]: UrlSegment}, + ): UrlSegmentGroup { const updatedSegments = this.createSegments(redirectTo, group.segments, segments, posParams); let children: {[n: string]: UrlSegmentGroup} = {}; @@ -122,22 +143,30 @@ export class ApplyRedirects { } createSegments( - redirectTo: string, redirectToSegments: UrlSegment[], actualSegments: UrlSegment[], - posParams: {[k: string]: UrlSegment}): UrlSegment[] { - return redirectToSegments.map( - s => s.path.startsWith(':') ? this.findPosParam(redirectTo, s, posParams) : - this.findOrReturn(s, actualSegments)); + redirectTo: string, + redirectToSegments: UrlSegment[], + actualSegments: UrlSegment[], + posParams: {[k: string]: UrlSegment}, + ): UrlSegment[] { + return redirectToSegments.map((s) => + s.path.startsWith(':') + ? this.findPosParam(redirectTo, s, posParams) + : this.findOrReturn(s, actualSegments), + ); } findPosParam( - redirectTo: string, redirectToUrlSegment: UrlSegment, - posParams: {[k: string]: UrlSegment}): UrlSegment { + redirectTo: string, + redirectToUrlSegment: UrlSegment, + posParams: {[k: string]: UrlSegment}, + ): UrlSegment { const pos = posParams[redirectToUrlSegment.path.substring(1)]; if (!pos) throw new RuntimeError( - RuntimeErrorCode.MISSING_REDIRECT, - (typeof ngDevMode === 'undefined' || ngDevMode) && - `Cannot redirect to '${redirectTo}'. Cannot find '${redirectToUrlSegment.path}'.`); + RuntimeErrorCode.MISSING_REDIRECT, + (typeof ngDevMode === 'undefined' || ngDevMode) && + `Cannot redirect to '${redirectTo}'. Cannot find '${redirectToUrlSegment.path}'.`, + ); return pos; } diff --git a/packages/router/src/components/empty_outlet.ts b/packages/router/src/components/empty_outlet.ts index 25f75675633..714ee5d6401 100644 --- a/packages/router/src/components/empty_outlet.ts +++ b/packages/router/src/components/empty_outlet.ts @@ -24,7 +24,6 @@ import {RouterOutlet} from '../directives/router_outlet'; imports: [RouterOutlet], standalone: true, }) -export class ɵEmptyOutletComponent { -} +export class ɵEmptyOutletComponent {} export {ɵEmptyOutletComponent as EmptyOutletComponent}; diff --git a/packages/router/src/create_router_state.ts b/packages/router/src/create_router_state.ts index f00d40f0ca1..c7a710e3e63 100644 --- a/packages/router/src/create_router_state.ts +++ b/packages/router/src/create_router_state.ts @@ -9,19 +9,28 @@ import {BehaviorSubject} from 'rxjs'; import {DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy'; -import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state'; +import { + ActivatedRoute, + ActivatedRouteSnapshot, + RouterState, + RouterStateSnapshot, +} from './router_state'; import {TreeNode} from './utils/tree'; export function createRouterState( - routeReuseStrategy: RouteReuseStrategy, curr: RouterStateSnapshot, - prevState: RouterState): RouterState { + routeReuseStrategy: RouteReuseStrategy, + curr: RouterStateSnapshot, + prevState: RouterState, +): RouterState { const root = createNode(routeReuseStrategy, curr._root, prevState ? prevState._root : undefined); return new RouterState(root, curr); } function createNode( - routeReuseStrategy: RouteReuseStrategy, curr: TreeNode, - prevState?: TreeNode): TreeNode { + routeReuseStrategy: RouteReuseStrategy, + curr: TreeNode, + prevState?: TreeNode, +): TreeNode { // reuse an activated route that is currently displayed on the screen if (prevState && routeReuseStrategy.shouldReuseRoute(curr.value, prevState.value.snapshot)) { const value = prevState.value; @@ -35,21 +44,23 @@ function createNode( if (detachedRouteHandle !== null) { const tree = (detachedRouteHandle as DetachedRouteHandleInternal).route; tree.value._futureSnapshot = curr.value; - tree.children = curr.children.map(c => createNode(routeReuseStrategy, c)); + tree.children = curr.children.map((c) => createNode(routeReuseStrategy, c)); return tree; } } const value = createActivatedRoute(curr.value); - const children = curr.children.map(c => createNode(routeReuseStrategy, c)); + const children = curr.children.map((c) => createNode(routeReuseStrategy, c)); return new TreeNode(value, children); } } function createOrReuseChildren( - routeReuseStrategy: RouteReuseStrategy, curr: TreeNode, - prevState: TreeNode) { - return curr.children.map(child => { + routeReuseStrategy: RouteReuseStrategy, + curr: TreeNode, + prevState: TreeNode, +) { + return curr.children.map((child) => { for (const p of prevState.children) { if (routeReuseStrategy.shouldReuseRoute(child.value, p.value.snapshot)) { return createNode(routeReuseStrategy, child, p); @@ -61,6 +72,13 @@ function createOrReuseChildren( function createActivatedRoute(c: ActivatedRouteSnapshot) { return new ActivatedRoute( - new BehaviorSubject(c.url), new BehaviorSubject(c.params), new BehaviorSubject(c.queryParams), - new BehaviorSubject(c.fragment), new BehaviorSubject(c.data), c.outlet, c.component, c); + new BehaviorSubject(c.url), + new BehaviorSubject(c.params), + new BehaviorSubject(c.queryParams), + new BehaviorSubject(c.fragment), + new BehaviorSubject(c.data), + c.outlet, + c.component, + c, + ); } diff --git a/packages/router/src/create_url_tree.ts b/packages/router/src/create_url_tree.ts index 361c2e52a76..26e43155166 100644 --- a/packages/router/src/create_url_tree.ts +++ b/packages/router/src/create_url_tree.ts @@ -14,7 +14,6 @@ import {Params, PRIMARY_OUTLET} from './shared'; import {createRoot, squashSegmentGroup, UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree'; import {last, shallowEqual} from './utils/collection'; - /** * Creates a `UrlTree` relative to an `ActivatedRouteSnapshot`. * @@ -67,17 +66,21 @@ import {last, shallowEqual} from './utils/collection'; * ``` */ export function createUrlTreeFromSnapshot( - relativeTo: ActivatedRouteSnapshot, commands: any[], queryParams: Params|null = null, - fragment: string|null = null): UrlTree { + relativeTo: ActivatedRouteSnapshot, + commands: any[], + queryParams: Params | null = null, + fragment: string | null = null, +): UrlTree { const relativeToUrlSegmentGroup = createSegmentGroupFromRoute(relativeTo); return createUrlTreeFromSegmentGroup(relativeToUrlSegmentGroup, commands, queryParams, fragment); } export function createSegmentGroupFromRoute(route: ActivatedRouteSnapshot): UrlSegmentGroup { - let targetGroup: UrlSegmentGroup|undefined; + let targetGroup: UrlSegmentGroup | undefined; - function createSegmentGroupFromRouteRecursive(currentRoute: ActivatedRouteSnapshot): - UrlSegmentGroup { + function createSegmentGroupFromRouteRecursive( + currentRoute: ActivatedRouteSnapshot, + ): UrlSegmentGroup { const childOutlets: {[outlet: string]: UrlSegmentGroup} = {}; for (const childSnapshot of currentRoute.children) { const root = createSegmentGroupFromRouteRecursive(childSnapshot); @@ -96,8 +99,11 @@ export function createSegmentGroupFromRoute(route: ActivatedRouteSnapshot): UrlS } export function createUrlTreeFromSegmentGroup( - relativeTo: UrlSegmentGroup, commands: any[], queryParams: Params|null, - fragment: string|null): UrlTree { + relativeTo: UrlSegmentGroup, + commands: any[], + queryParams: Params | null, + fragment: string | null, +): UrlTree { let root = relativeTo; while (root.parent) { root = root.parent; @@ -116,9 +122,9 @@ export function createUrlTreeFromSegmentGroup( } const position = findStartingPositionForTargetGroup(nav, root, relativeTo); - const newSegmentGroup = position.processChildren ? - updateSegmentGroupChildren(position.segmentGroup, position.index, nav.commands) : - updateSegmentGroup(position.segmentGroup, position.index, nav.commands); + const newSegmentGroup = position.processChildren + ? updateSegmentGroupChildren(position.segmentGroup, position.index, nav.commands) + : updateSegmentGroup(position.segmentGroup, position.index, nav.commands); return tree(root, position.segmentGroup, newSegmentGroup, queryParams, fragment); } @@ -135,8 +141,12 @@ function isCommandWithOutlets(command: any): command is {outlets: {[key: string] } function tree( - oldRoot: UrlSegmentGroup, oldSegmentGroup: UrlSegmentGroup, newSegmentGroup: UrlSegmentGroup, - queryParams: Params|null, fragment: string|null): UrlTree { + oldRoot: UrlSegmentGroup, + oldSegmentGroup: UrlSegmentGroup, + newSegmentGroup: UrlSegmentGroup, + queryParams: Params | null, + fragment: string | null, +): UrlTree { let qp: any = {}; if (queryParams) { Object.entries(queryParams).forEach(([name, value]) => { @@ -163,8 +173,10 @@ function tree( * value. */ function replaceSegment( - current: UrlSegmentGroup, oldSegment: UrlSegmentGroup, - newSegment: UrlSegmentGroup): UrlSegmentGroup { + current: UrlSegmentGroup, + oldSegment: UrlSegmentGroup, + newSegment: UrlSegmentGroup, +): UrlSegmentGroup { const children: {[key: string]: UrlSegmentGroup} = {}; Object.entries(current.children).forEach(([outletName, c]) => { if (c === oldSegment) { @@ -178,20 +190,25 @@ function replaceSegment( class Navigation { constructor( - public isAbsolute: boolean, public numberOfDoubleDots: number, public commands: any[]) { + public isAbsolute: boolean, + public numberOfDoubleDots: number, + public commands: any[], + ) { if (isAbsolute && commands.length > 0 && isMatrixParams(commands[0])) { throw new RuntimeError( - RuntimeErrorCode.ROOT_SEGMENT_MATRIX_PARAMS, - (typeof ngDevMode === 'undefined' || ngDevMode) && - 'Root segment cannot have matrix parameters'); + RuntimeErrorCode.ROOT_SEGMENT_MATRIX_PARAMS, + (typeof ngDevMode === 'undefined' || ngDevMode) && + 'Root segment cannot have matrix parameters', + ); } const cmdWithOutlet = commands.find(isCommandWithOutlets); if (cmdWithOutlet && cmdWithOutlet !== last(commands)) { throw new RuntimeError( - RuntimeErrorCode.MISPLACED_OUTLETS_COMMAND, - (typeof ngDevMode === 'undefined' || ngDevMode) && - '{outlets:{}} has to be the last command'); + RuntimeErrorCode.MISPLACED_OUTLETS_COMMAND, + (typeof ngDevMode === 'undefined' || ngDevMode) && + '{outlets:{}} has to be the last command', + ); } } @@ -202,7 +219,7 @@ class Navigation { /** Transforms commands to a normalized `Navigation` */ function computeNavigation(commands: any[]): Navigation { - if ((typeof commands[0] === 'string') && commands.length === 1 && commands[0] === '/') { + if (typeof commands[0] === 'string' && commands.length === 1 && commands[0] === '/') { return new Navigation(true, 0, commands); } @@ -232,9 +249,11 @@ function computeNavigation(commands: any[]): Navigation { cmd.split('/').forEach((urlPart, partIndex) => { if (partIndex == 0 && urlPart === '.') { // skip './a' - } else if (partIndex == 0 && urlPart === '') { // '/a' + } else if (partIndex == 0 && urlPart === '') { + // '/a' isAbsolute = true; - } else if (urlPart === '..') { // '../a' + } else if (urlPart === '..') { + // '../a' numberOfDoubleDots++; } else if (urlPart != '') { res.push(urlPart); @@ -252,12 +271,17 @@ function computeNavigation(commands: any[]): Navigation { class Position { constructor( - public segmentGroup: UrlSegmentGroup, public processChildren: boolean, public index: number) { - } + public segmentGroup: UrlSegmentGroup, + public processChildren: boolean, + public index: number, + ) {} } function findStartingPositionForTargetGroup( - nav: Navigation, root: UrlSegmentGroup, target: UrlSegmentGroup): Position { + nav: Navigation, + root: UrlSegmentGroup, + target: UrlSegmentGroup, +): Position { if (nav.isAbsolute) { return new Position(root, true, 0); } @@ -279,7 +303,10 @@ function findStartingPositionForTargetGroup( } function createPositionApplyingDoubleDots( - group: UrlSegmentGroup, index: number, numberOfDoubleDots: number): Position { + group: UrlSegmentGroup, + index: number, + numberOfDoubleDots: number, +): Position { let g = group; let ci = index; let dd = numberOfDoubleDots; @@ -288,15 +315,16 @@ function createPositionApplyingDoubleDots( g = g.parent!; if (!g) { throw new RuntimeError( - RuntimeErrorCode.INVALID_DOUBLE_DOTS, - (typeof ngDevMode === 'undefined' || ngDevMode) && 'Invalid number of \'../\''); + RuntimeErrorCode.INVALID_DOUBLE_DOTS, + (typeof ngDevMode === 'undefined' || ngDevMode) && "Invalid number of '../'", + ); } ci = g.segments.length; } return new Position(g, false, ci - dd); } -function getOutlets(commands: unknown[]): {[k: string]: unknown[]|string} { +function getOutlets(commands: unknown[]): {[k: string]: unknown[] | string} { if (isCommandWithOutlets(commands[0])) { return commands[0].outlets; } @@ -305,7 +333,10 @@ function getOutlets(commands: unknown[]): {[k: string]: unknown[]|string} { } function updateSegmentGroup( - segmentGroup: UrlSegmentGroup|undefined, startIndex: number, commands: any[]): UrlSegmentGroup { + segmentGroup: UrlSegmentGroup | undefined, + startIndex: number, + commands: any[], +): UrlSegmentGroup { segmentGroup ??= new UrlSegmentGroup([], {}); if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) { return updateSegmentGroupChildren(segmentGroup, startIndex, commands); @@ -315,8 +346,10 @@ function updateSegmentGroup( const slicedCommands = commands.slice(m.commandIndex); if (m.match && m.pathIndex < segmentGroup.segments.length) { const g = new UrlSegmentGroup(segmentGroup.segments.slice(0, m.pathIndex), {}); - g.children[PRIMARY_OUTLET] = - new UrlSegmentGroup(segmentGroup.segments.slice(m.pathIndex), segmentGroup.children); + g.children[PRIMARY_OUTLET] = new UrlSegmentGroup( + segmentGroup.segments.slice(m.pathIndex), + segmentGroup.children, + ); return updateSegmentGroupChildren(g, 0, slicedCommands); } else if (m.match && slicedCommands.length === 0) { return new UrlSegmentGroup(segmentGroup.segments, {}); @@ -330,7 +363,10 @@ function updateSegmentGroup( } function updateSegmentGroupChildren( - segmentGroup: UrlSegmentGroup, startIndex: number, commands: any[]): UrlSegmentGroup { + segmentGroup: UrlSegmentGroup, + startIndex: number, + commands: any[], +): UrlSegmentGroup { if (commands.length === 0) { return new UrlSegmentGroup(segmentGroup.segments, {}); } else { @@ -357,11 +393,17 @@ function updateSegmentGroupChildren( // `UrlSegmentGroup` that is created from an "unsquashed"/expanded `ActivatedRoute` tree. // This code effectively "squashes" empty path primary routes when they have no siblings on // the same level of the tree. - if (Object.keys(outlets).some(o => o !== PRIMARY_OUTLET) && - segmentGroup.children[PRIMARY_OUTLET] && segmentGroup.numberOfChildren === 1 && - segmentGroup.children[PRIMARY_OUTLET].segments.length === 0) { - const childrenOfEmptyChild = - updateSegmentGroupChildren(segmentGroup.children[PRIMARY_OUTLET], startIndex, commands); + if ( + Object.keys(outlets).some((o) => o !== PRIMARY_OUTLET) && + segmentGroup.children[PRIMARY_OUTLET] && + segmentGroup.numberOfChildren === 1 && + segmentGroup.children[PRIMARY_OUTLET].segments.length === 0 + ) { + const childrenOfEmptyChild = updateSegmentGroupChildren( + segmentGroup.children[PRIMARY_OUTLET], + startIndex, + commands, + ); return new UrlSegmentGroup(segmentGroup.segments, childrenOfEmptyChild.children); } @@ -400,11 +442,11 @@ function prefixedWith(segmentGroup: UrlSegmentGroup, startIndex: number, command } const curr = `${command}`; const next = - currentCommandIndex < commands.length - 1 ? commands[currentCommandIndex + 1] : null; + currentCommandIndex < commands.length - 1 ? commands[currentCommandIndex + 1] : null; if (currentPathIndex > 0 && curr === undefined) break; - if (curr && next && (typeof next === 'object') && next.outlets === undefined) { + if (curr && next && typeof next === 'object' && next.outlets === undefined) { if (!compare(curr, next, path)) return noMatch; currentCommandIndex += 2; } else { @@ -418,7 +460,10 @@ function prefixedWith(segmentGroup: UrlSegmentGroup, startIndex: number, command } function createNewSegmentGroup( - segmentGroup: UrlSegmentGroup, startIndex: number, commands: any[]): UrlSegmentGroup { + segmentGroup: UrlSegmentGroup, + startIndex: number, + commands: any[], +): UrlSegmentGroup { const paths = segmentGroup.segments.slice(0, startIndex); let i = 0; @@ -438,7 +483,7 @@ function createNewSegmentGroup( } const curr = isCommandWithOutlets(command) ? command.outlets[PRIMARY_OUTLET] : `${command}`; - const next = (i < commands.length - 1) ? commands[i + 1] : null; + const next = i < commands.length - 1 ? commands[i + 1] : null; if (curr && next && isMatrixParams(next)) { paths.push(new UrlSegment(curr, stringify(next))); i += 2; @@ -450,8 +495,9 @@ function createNewSegmentGroup( return new UrlSegmentGroup(paths, {}); } -function createNewSegmentChildren(outlets: {[name: string]: unknown[]|string}): - {[outlet: string]: UrlSegmentGroup} { +function createNewSegmentChildren(outlets: {[name: string]: unknown[] | string}): { + [outlet: string]: UrlSegmentGroup; +} { const children: {[outlet: string]: UrlSegmentGroup} = {}; Object.entries(outlets).forEach(([outlet, commands]) => { if (typeof commands === 'string') { @@ -466,7 +512,7 @@ function createNewSegmentChildren(outlets: {[name: string]: unknown[]|string}): function stringify(params: {[key: string]: any}): {[key: string]: string} { const res: {[key: string]: string} = {}; - Object.entries(params).forEach(([k, v]) => res[k] = `${v}`); + Object.entries(params).forEach(([k, v]) => (res[k] = `${v}`)); return res; } diff --git a/packages/router/src/directives/router_link.ts b/packages/router/src/directives/router_link.ts index e9ae60ef1ba..c5b3ce5a05f 100644 --- a/packages/router/src/directives/router_link.ts +++ b/packages/router/src/directives/router_link.ts @@ -7,7 +7,20 @@ */ import {LocationStrategy} from '@angular/common'; -import {Attribute, booleanAttribute, Directive, ElementRef, HostBinding, HostListener, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges, ɵɵsanitizeUrlOrResourceUrl} from '@angular/core'; +import { + Attribute, + booleanAttribute, + Directive, + ElementRef, + HostBinding, + HostListener, + Input, + OnChanges, + OnDestroy, + Renderer2, + SimpleChanges, + ɵɵsanitizeUrlOrResourceUrl, +} from '@angular/core'; import {Subject, Subscription} from 'rxjs'; import {Event, NavigationEnd} from '../events'; @@ -17,7 +30,6 @@ import {ActivatedRoute} from '../router_state'; import {Params} from '../shared'; import {UrlTree} from '../url_tree'; - /** * @description * @@ -124,7 +136,7 @@ export class RouterLink implements OnChanges, OnDestroy { * Represents an `href` attribute value applied to a host element, * when a host element is ``. For other tags, the value is `null`. */ - href: string|null = null; + href: string | null = null; /** * Represents the `target` attribute on a host element. @@ -138,7 +150,7 @@ export class RouterLink implements OnChanges, OnDestroy { * @see {@link UrlCreationOptions#queryParams} * @see {@link Router#createUrlTree} */ - @Input() queryParams?: Params|null; + @Input() queryParams?: Params | null; /** * Passed to {@link Router#createUrlTree} as part of the * `UrlCreationOptions`. @@ -152,7 +164,7 @@ export class RouterLink implements OnChanges, OnDestroy { * @see {@link UrlCreationOptions#queryParamsHandling} * @see {@link Router#createUrlTree} */ - @Input() queryParamsHandling?: QueryParamsHandling|null; + @Input() queryParamsHandling?: QueryParamsHandling | null; /** * Passed to {@link Router#navigateByUrl} as part of the * `NavigationBehaviorOptions`. @@ -176,9 +188,9 @@ export class RouterLink implements OnChanges, OnDestroy { * @see {@link UrlCreationOptions#relativeTo} * @see {@link Router#createUrlTree} */ - @Input() relativeTo?: ActivatedRoute|null; + @Input() relativeTo?: ActivatedRoute | null; - private commands: any[]|null = null; + private commands: any[] | null = null; /** Whether a host element is an `` tag. */ private isAnchorElement: boolean; @@ -189,10 +201,13 @@ export class RouterLink implements OnChanges, OnDestroy { onChanges = new Subject(); constructor( - private router: Router, private route: ActivatedRoute, - @Attribute('tabindex') private readonly tabIndexAttribute: string|null|undefined, - private readonly renderer: Renderer2, private readonly el: ElementRef, - private locationStrategy?: LocationStrategy) { + private router: Router, + private route: ActivatedRoute, + @Attribute('tabindex') private readonly tabIndexAttribute: string | null | undefined, + private readonly renderer: Renderer2, + private readonly el: ElementRef, + private locationStrategy?: LocationStrategy, + ) { const tagName = el.nativeElement.tagName?.toLowerCase(); this.isAnchorElement = tagName === 'a' || tagName === 'area'; @@ -235,7 +250,7 @@ export class RouterLink implements OnChanges, OnDestroy { * Modifies the tab index if there was not a tabindex attribute on the element during * instantiation. */ - private setTabIndexIfNotOnNativeEl(newTabIndex: string|null) { + private setTabIndexIfNotOnNativeEl(newTabIndex: string | null) { if (this.tabIndexAttribute != null /* both `null` and `undefined` */ || this.isAnchorElement) { return; } @@ -260,7 +275,7 @@ export class RouterLink implements OnChanges, OnDestroy { * @see {@link Router#createUrlTree} */ @Input() - set routerLink(commands: any[]|string|null|undefined) { + set routerLink(commands: any[] | string | null | undefined) { if (commands != null) { this.commands = Array.isArray(commands) ? commands : [commands]; this.setTabIndexIfNotOnNativeEl('0'); @@ -271,13 +286,22 @@ export class RouterLink implements OnChanges, OnDestroy { } /** @nodoc */ - @HostListener( - 'click', - ['$event.button', '$event.ctrlKey', '$event.shiftKey', '$event.altKey', '$event.metaKey']) - onClick(button: number, ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean): - boolean { + @HostListener('click', [ + '$event.button', + '$event.ctrlKey', + '$event.shiftKey', + '$event.altKey', + '$event.metaKey', + ]) + onClick( + button: number, + ctrlKey: boolean, + shiftKey: boolean, + altKey: boolean, + metaKey: boolean, + ): boolean { const urlTree = this.urlTree; - + if (urlTree === null) { return true; } @@ -313,27 +337,33 @@ export class RouterLink implements OnChanges, OnDestroy { private updateHref(): void { const urlTree = this.urlTree; - this.href = urlTree !== null && this.locationStrategy ? - this.locationStrategy?.prepareExternalUrl(this.router.serializeUrl(urlTree)) : - null; + this.href = + urlTree !== null && this.locationStrategy + ? this.locationStrategy?.prepareExternalUrl(this.router.serializeUrl(urlTree)) + : null; - const sanitizedValue = this.href === null ? - null : - // This class represents a directive that can be added to both `` elements, - // as well as other elements. As a result, we can't define security context at - // compile time. So the security context is deferred to runtime. - // The `ɵɵsanitizeUrlOrResourceUrl` selects the necessary sanitizer function - // based on the tag and property names. The logic mimics the one from - // `packages/compiler/src/schema/dom_security_schema.ts`, which is used at compile time. - // - // Note: we should investigate whether we can switch to using `@HostBinding('attr.href')` - // instead of applying a value via a renderer, after a final merge of the - // `RouterLinkWithHref` directive. - ɵɵsanitizeUrlOrResourceUrl(this.href, this.el.nativeElement.tagName.toLowerCase(), 'href'); + const sanitizedValue = + this.href === null + ? null + : // This class represents a directive that can be added to both `` elements, + // as well as other elements. As a result, we can't define security context at + // compile time. So the security context is deferred to runtime. + // The `ɵɵsanitizeUrlOrResourceUrl` selects the necessary sanitizer function + // based on the tag and property names. The logic mimics the one from + // `packages/compiler/src/schema/dom_security_schema.ts`, which is used at compile time. + // + // Note: we should investigate whether we can switch to using `@HostBinding('attr.href')` + // instead of applying a value via a renderer, after a final merge of the + // `RouterLinkWithHref` directive. + ɵɵsanitizeUrlOrResourceUrl( + this.href, + this.el.nativeElement.tagName.toLowerCase(), + 'href', + ); this.applyAttributeValue('href', sanitizedValue); } - private applyAttributeValue(attrName: string, attrValue: string|null) { + private applyAttributeValue(attrName: string, attrValue: string | null) { const renderer = this.renderer; const nativeElement = this.el.nativeElement; if (attrValue !== null) { @@ -343,7 +373,7 @@ export class RouterLink implements OnChanges, OnDestroy { } } - get urlTree(): UrlTree|null { + get urlTree(): UrlTree | null { if (this.commands === null) { return null; } diff --git a/packages/router/src/directives/router_link_active.ts b/packages/router/src/directives/router_link_active.ts index 11a6ccca4bd..13414e996e7 100644 --- a/packages/router/src/directives/router_link_active.ts +++ b/packages/router/src/directives/router_link_active.ts @@ -6,7 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ -import {AfterContentInit, ChangeDetectorRef, ContentChildren, Directive, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Optional, Output, QueryList, Renderer2, SimpleChanges} from '@angular/core'; +import { + AfterContentInit, + ChangeDetectorRef, + ContentChildren, + Directive, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnDestroy, + Optional, + Output, + QueryList, + Renderer2, + SimpleChanges, +} from '@angular/core'; import {from, of, Subscription} from 'rxjs'; import {mergeAll} from 'rxjs/operators'; @@ -16,7 +31,6 @@ import {IsActiveMatchOptions} from '../url_tree'; import {RouterLink} from './router_link'; - /** * * @description @@ -110,8 +124,7 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit * * @see {@link Router#isActive} */ - @Input() routerLinkActiveOptions: {exact: boolean}|IsActiveMatchOptions = {exact: false}; - + @Input() routerLinkActiveOptions: {exact: boolean} | IsActiveMatchOptions = {exact: false}; /** * Aria-current attribute to apply when the router link is active. @@ -120,7 +133,7 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit * * @see {@link https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current} */ - @Input() ariaCurrentWhenActive?: 'page'|'step'|'location'|'date'|'time'|true|false; + @Input() ariaCurrentWhenActive?: 'page' | 'step' | 'location' | 'date' | 'time' | true | false; /** * @@ -141,8 +154,12 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit @Output() readonly isActiveChange: EventEmitter = new EventEmitter(); constructor( - private router: Router, private element: ElementRef, private renderer: Renderer2, - private readonly cdr: ChangeDetectorRef, @Optional() private link?: RouterLink) { + private router: Router, + private element: ElementRef, + private renderer: Renderer2, + private readonly cdr: ChangeDetectorRef, + @Optional() private link?: RouterLink, + ) { this.routerEventsSubscription = router.events.subscribe((s: Event) => { if (s instanceof NavigationEnd) { this.update(); @@ -153,28 +170,32 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit /** @nodoc */ ngAfterContentInit(): void { // `of(null)` is used to force subscribe body to execute once immediately (like `startWith`). - of(this.links.changes, of(null)).pipe(mergeAll()).subscribe(_ => { - this.update(); - this.subscribeToEachLinkOnChanges(); - }); + of(this.links.changes, of(null)) + .pipe(mergeAll()) + .subscribe((_) => { + this.update(); + this.subscribeToEachLinkOnChanges(); + }); } private subscribeToEachLinkOnChanges() { this.linkInputChangesSubscription?.unsubscribe(); const allLinkChanges = [...this.links.toArray(), this.link] - .filter((link): link is RouterLink => !!link) - .map(link => link.onChanges); - this.linkInputChangesSubscription = from(allLinkChanges).pipe(mergeAll()).subscribe(link => { - if (this._isActive !== this.isLinkActive(this.router)(link)) { - this.update(); - } - }); + .filter((link): link is RouterLink => !!link) + .map((link) => link.onChanges); + this.linkInputChangesSubscription = from(allLinkChanges) + .pipe(mergeAll()) + .subscribe((link) => { + if (this._isActive !== this.isLinkActive(this.router)(link)) { + this.update(); + } + }); } @Input() - set routerLinkActive(data: string[]|string) { + set routerLinkActive(data: string[] | string) { const classes = Array.isArray(data) ? data : data.split(' '); - this.classes = classes.filter(c => !!c); + this.classes = classes.filter((c) => !!c); } /** @nodoc */ @@ -203,7 +224,10 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit }); if (hasActiveLinks && this.ariaCurrentWhenActive !== undefined) { this.renderer.setAttribute( - this.element.nativeElement, 'aria-current', this.ariaCurrentWhenActive.toString()); + this.element.nativeElement, + 'aria-current', + this.ariaCurrentWhenActive.toString(), + ); } else { this.renderer.removeAttribute(this.element.nativeElement, 'aria-current'); } @@ -215,11 +239,12 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit } private isLinkActive(router: Router): (link: RouterLink) => boolean { - const options: boolean|IsActiveMatchOptions = - isActiveMatchOptions(this.routerLinkActiveOptions) ? - this.routerLinkActiveOptions : - // While the types should disallow `undefined` here, it's possible without strict inputs - (this.routerLinkActiveOptions.exact || false); + const options: boolean | IsActiveMatchOptions = isActiveMatchOptions( + this.routerLinkActiveOptions, + ) + ? this.routerLinkActiveOptions + : // While the types should disallow `undefined` here, it's possible without strict inputs + this.routerLinkActiveOptions.exact || false; return (link: RouterLink) => { const urlTree = link.urlTree; return urlTree ? router.isActive(urlTree, options) : false; @@ -228,14 +253,15 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit private hasActiveLinks(): boolean { const isActiveCheckFn = this.isLinkActive(this.router); - return this.link && isActiveCheckFn(this.link) || this.links.some(isActiveCheckFn); + return (this.link && isActiveCheckFn(this.link)) || this.links.some(isActiveCheckFn); } } /** * Use instead of `'paths' in options` to be compatible with property renaming */ -function isActiveMatchOptions(options: {exact: boolean}| - IsActiveMatchOptions): options is IsActiveMatchOptions { +function isActiveMatchOptions( + options: {exact: boolean} | IsActiveMatchOptions, +): options is IsActiveMatchOptions { return !!(options as IsActiveMatchOptions).paths; } diff --git a/packages/router/src/directives/router_outlet.ts b/packages/router/src/directives/router_outlet.ts index 42826271290..5717f0bd5e4 100644 --- a/packages/router/src/directives/router_outlet.ts +++ b/packages/router/src/directives/router_outlet.ts @@ -6,7 +6,25 @@ * found in the LICENSE file at https://angular.io/license */ -import {ChangeDetectorRef, ComponentRef, Directive, EnvironmentInjector, EventEmitter, inject, Injectable, InjectionToken, Injector, Input, OnDestroy, OnInit, Output, reflectComponentType, SimpleChanges, ViewContainerRef, ɵRuntimeError as RuntimeError,} from '@angular/core'; +import { + ChangeDetectorRef, + ComponentRef, + Directive, + EnvironmentInjector, + EventEmitter, + inject, + Injectable, + InjectionToken, + Injector, + Input, + OnDestroy, + OnInit, + Output, + reflectComponentType, + SimpleChanges, + ViewContainerRef, + ɵRuntimeError as RuntimeError, +} from '@angular/core'; import {combineLatest, of, Subscription} from 'rxjs'; import {switchMap} from 'rxjs/operators'; @@ -16,7 +34,6 @@ import {ChildrenOutletContexts} from '../router_outlet_context'; import {ActivatedRoute} from '../router_state'; import {PRIMARY_OUTLET} from '../shared'; - /** * An interface that defines the contract for developing a component outlet for the `Router`. * @@ -39,7 +56,7 @@ export interface RouterOutletContract { isActivated: boolean; /** The instance of the activated component or `null` if the outlet is not activated. */ - component: Object|null; + component: Object | null; /** * The `Data` of the `ActivatedRoute` snapshot. @@ -49,12 +66,15 @@ export interface RouterOutletContract { /** * The `ActivatedRoute` for the outlet or `null` if the outlet is not activated. */ - activatedRoute: ActivatedRoute|null; + activatedRoute: ActivatedRoute | null; /** * Called by the `Router` when the outlet should activate (create a component). */ - activateWith(activatedRoute: ActivatedRoute, environmentInjector: EnvironmentInjector|null): void; + activateWith( + activatedRoute: ActivatedRoute, + environmentInjector: EnvironmentInjector | null, + ): void; /** * A request to destroy the currently activated component. @@ -167,12 +187,12 @@ export interface RouterOutletContract { standalone: true, }) export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract { - private activated: ComponentRef|null = null; + private activated: ComponentRef | null = null; /** @internal */ - get activatedComponentRef(): ComponentRef|null { + get activatedComponentRef(): ComponentRef | null { return this.activated; } - private _activatedRoute: ActivatedRoute|null = null; + private _activatedRoute: ActivatedRoute | null = null; /** * The name of the outlet * @@ -270,16 +290,18 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract { get component(): Object { if (!this.activated) throw new RuntimeError( - RuntimeErrorCode.OUTLET_NOT_ACTIVATED, - (typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated'); + RuntimeErrorCode.OUTLET_NOT_ACTIVATED, + (typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated', + ); return this.activated.instance; } get activatedRoute(): ActivatedRoute { if (!this.activated) throw new RuntimeError( - RuntimeErrorCode.OUTLET_NOT_ACTIVATED, - (typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated'); + RuntimeErrorCode.OUTLET_NOT_ACTIVATED, + (typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated', + ); return this._activatedRoute as ActivatedRoute; } @@ -296,8 +318,9 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract { detach(): ComponentRef { if (!this.activated) throw new RuntimeError( - RuntimeErrorCode.OUTLET_NOT_ACTIVATED, - (typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated'); + RuntimeErrorCode.OUTLET_NOT_ACTIVATED, + (typeof ngDevMode === 'undefined' || ngDevMode) && 'Outlet is not activated', + ); this.location.detach(); const cmp = this.activated; this.activated = null; @@ -327,12 +350,13 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract { } } - activateWith(activatedRoute: ActivatedRoute, environmentInjector?: EnvironmentInjector|null) { + activateWith(activatedRoute: ActivatedRoute, environmentInjector?: EnvironmentInjector | null) { if (this.isActivated) { throw new RuntimeError( - RuntimeErrorCode.OUTLET_ALREADY_ACTIVATED, - (typeof ngDevMode === 'undefined' || ngDevMode) && - 'Cannot activate an already activated outlet'); + RuntimeErrorCode.OUTLET_ALREADY_ACTIVATED, + (typeof ngDevMode === 'undefined' || ngDevMode) && + 'Cannot activate an already activated outlet', + ); } this._activatedRoute = activatedRoute; const location = this.location; @@ -344,7 +368,7 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract { this.activated = location.createComponent(component, { index: location.length, injector, - environmentInjector: environmentInjector ?? this.environmentInjector + environmentInjector: environmentInjector ?? this.environmentInjector, }); // Calling `markForCheck` to make sure we will run the change detection when the // `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component. @@ -356,8 +380,10 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract { class OutletInjector implements Injector { constructor( - private route: ActivatedRoute, private childContexts: ChildrenOutletContexts, - private parent: Injector) {} + private route: ActivatedRoute, + private childContexts: ChildrenOutletContexts, + private parent: Injector, + ) {} get(token: any, notFoundValue?: any): any { if (token === ActivatedRoute) { @@ -390,7 +416,7 @@ export const INPUT_BINDER = new InjectionToken(''); */ @Injectable() export class RoutedComponentInputBinder { - private outletDataSubscriptions = new Map; + private outletDataSubscriptions = new Map(); bindActivatedRouteToOutletComponent(outlet: RouterOutlet) { this.unsubscribeFromRouteData(outlet); @@ -404,43 +430,48 @@ export class RoutedComponentInputBinder { private subscribeToRouteData(outlet: RouterOutlet) { const {activatedRoute} = outlet; - const dataSubscription = - combineLatest([ - activatedRoute.queryParams, - activatedRoute.params, - activatedRoute.data, - ]) - .pipe(switchMap(([queryParams, params, data], index) => { - data = {...queryParams, ...params, ...data}; - // Get the first result from the data subscription synchronously so it's available to - // the component as soon as possible (and doesn't require a second change detection). - if (index === 0) { - return of(data); - } - // Promise.resolve is used to avoid synchronously writing the wrong data when - // two of the Observables in the `combineLatest` stream emit one after - // another. - return Promise.resolve(data); - })) - .subscribe(data => { - // Outlet may have been deactivated or changed names to be associated with a different - // route - if (!outlet.isActivated || !outlet.activatedComponentRef || - outlet.activatedRoute !== activatedRoute || activatedRoute.component === null) { - this.unsubscribeFromRouteData(outlet); - return; - } + const dataSubscription = combineLatest([ + activatedRoute.queryParams, + activatedRoute.params, + activatedRoute.data, + ]) + .pipe( + switchMap(([queryParams, params, data], index) => { + data = {...queryParams, ...params, ...data}; + // Get the first result from the data subscription synchronously so it's available to + // the component as soon as possible (and doesn't require a second change detection). + if (index === 0) { + return of(data); + } + // Promise.resolve is used to avoid synchronously writing the wrong data when + // two of the Observables in the `combineLatest` stream emit one after + // another. + return Promise.resolve(data); + }), + ) + .subscribe((data) => { + // Outlet may have been deactivated or changed names to be associated with a different + // route + if ( + !outlet.isActivated || + !outlet.activatedComponentRef || + outlet.activatedRoute !== activatedRoute || + activatedRoute.component === null + ) { + this.unsubscribeFromRouteData(outlet); + return; + } - const mirror = reflectComponentType(activatedRoute.component); - if (!mirror) { - this.unsubscribeFromRouteData(outlet); - return; - } + const mirror = reflectComponentType(activatedRoute.component); + if (!mirror) { + this.unsubscribeFromRouteData(outlet); + return; + } - for (const {templateName} of mirror.inputs) { - outlet.activatedComponentRef.setInput(templateName, data[templateName]); - } - }); + for (const {templateName} of mirror.inputs) { + outlet.activatedComponentRef.setInput(templateName, data[templateName]); + } + }); this.outletDataSubscriptions.set(outlet, dataSubscription); } diff --git a/packages/router/src/events.ts b/packages/router/src/events.ts index d56d8c24186..2b64d3fcb5f 100644 --- a/packages/router/src/events.ts +++ b/packages/router/src/events.ts @@ -19,7 +19,7 @@ import {UrlTree} from './url_tree'; * * @publicApi */ -export type NavigationTrigger = 'imperative'|'popstate'|'hashchange'; +export type NavigationTrigger = 'imperative' | 'popstate' | 'hashchange'; export const IMPERATIVE_NAVIGATION = 'imperative'; /** @@ -73,10 +73,11 @@ export enum EventType { */ export class RouterEvent { constructor( - /** A unique ID that the router assigns to every router navigation. */ - public id: number, - /** The URL that is the destination for this navigation. */ - public url: string) {} + /** A unique ID that the router assigns to every router navigation. */ + public id: number, + /** The URL that is the destination for this navigation. */ + public url: string, + ) {} } /** @@ -114,17 +115,18 @@ export class NavigationStart extends RouterEvent { * remembered state, such as scroll position. * */ - restoredState?: {[k: string]: any, navigationId: number}|null; + restoredState?: {[k: string]: any; navigationId: number} | null; constructor( - /** @docsNotRequired */ - id: number, - /** @docsNotRequired */ - url: string, - /** @docsNotRequired */ - navigationTrigger: NavigationTrigger = 'imperative', - /** @docsNotRequired */ - restoredState: {[k: string]: any, navigationId: number}|null = null) { + /** @docsNotRequired */ + id: number, + /** @docsNotRequired */ + url: string, + /** @docsNotRequired */ + navigationTrigger: NavigationTrigger = 'imperative', + /** @docsNotRequired */ + restoredState: {[k: string]: any; navigationId: number} | null = null, + ) { super(id, url); this.navigationTrigger = navigationTrigger; this.restoredState = restoredState; @@ -149,19 +151,19 @@ export class NavigationEnd extends RouterEvent { readonly type = EventType.NavigationEnd; constructor( - /** @docsNotRequired */ - id: number, - /** @docsNotRequired */ - url: string, - /** @docsNotRequired */ - public urlAfterRedirects: string) { + /** @docsNotRequired */ + id: number, + /** @docsNotRequired */ + url: string, + /** @docsNotRequired */ + public urlAfterRedirects: string, + ) { super(id, url); } /** @docsNotRequired */ override toString(): string { - return `NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${ - this.urlAfterRedirects}')`; + return `NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}')`; } } @@ -225,21 +227,22 @@ export class NavigationCancel extends RouterEvent { readonly type = EventType.NavigationCancel; constructor( - /** @docsNotRequired */ - id: number, - /** @docsNotRequired */ - url: string, - /** - * A description of why the navigation was cancelled. For debug purposes only. Use `code` - * instead for a stable cancellation reason that can be used in production. - */ - public reason: string, - /** - * A code to indicate why the navigation was canceled. This cancellation code is stable for - * the reason and can be relied on whereas the `reason` string could change and should not be - * used in production. - */ - readonly code?: NavigationCancellationCode) { + /** @docsNotRequired */ + id: number, + /** @docsNotRequired */ + url: string, + /** + * A description of why the navigation was cancelled. For debug purposes only. Use `code` + * instead for a stable cancellation reason that can be used in production. + */ + public reason: string, + /** + * A code to indicate why the navigation was canceled. This cancellation code is stable for + * the reason and can be relied on whereas the `reason` string could change and should not be + * used in production. + */ + readonly code?: NavigationCancellationCode, + ) { super(id, url); } @@ -261,21 +264,22 @@ export class NavigationSkipped extends RouterEvent { readonly type = EventType.NavigationSkipped; constructor( - /** @docsNotRequired */ - id: number, - /** @docsNotRequired */ - url: string, - /** - * A description of why the navigation was skipped. For debug purposes only. Use `code` - * instead for a stable skipped reason that can be used in production. - */ - public reason: string, - /** - * A code to indicate why the navigation was skipped. This code is stable for - * the reason and can be relied on whereas the `reason` string could change and should not be - * used in production. - */ - readonly code?: NavigationSkippedCode) { + /** @docsNotRequired */ + id: number, + /** @docsNotRequired */ + url: string, + /** + * A description of why the navigation was skipped. For debug purposes only. Use `code` + * instead for a stable skipped reason that can be used in production. + */ + public reason: string, + /** + * A code to indicate why the navigation was skipped. This code is stable for + * the reason and can be relied on whereas the `reason` string could change and should not be + * used in production. + */ + readonly code?: NavigationSkippedCode, + ) { super(id, url); } } @@ -293,19 +297,20 @@ export class NavigationError extends RouterEvent { readonly type = EventType.NavigationError; constructor( - /** @docsNotRequired */ - id: number, - /** @docsNotRequired */ - url: string, - /** @docsNotRequired */ - public error: any, - /** - * The target of the navigation when the error occurred. - * - * Note that this can be `undefined` because an error could have occurred before the - * `RouterStateSnapshot` was created for the navigation. - */ - readonly target?: RouterStateSnapshot) { + /** @docsNotRequired */ + id: number, + /** @docsNotRequired */ + url: string, + /** @docsNotRequired */ + public error: any, + /** + * The target of the navigation when the error occurred. + * + * Note that this can be `undefined` because an error could have occurred before the + * `RouterStateSnapshot` was created for the navigation. + */ + readonly target?: RouterStateSnapshot, + ) { super(id, url); } @@ -324,21 +329,21 @@ export class RoutesRecognized extends RouterEvent { readonly type = EventType.RoutesRecognized; constructor( - /** @docsNotRequired */ - id: number, - /** @docsNotRequired */ - url: string, - /** @docsNotRequired */ - public urlAfterRedirects: string, - /** @docsNotRequired */ - public state: RouterStateSnapshot) { + /** @docsNotRequired */ + id: number, + /** @docsNotRequired */ + url: string, + /** @docsNotRequired */ + public urlAfterRedirects: string, + /** @docsNotRequired */ + public state: RouterStateSnapshot, + ) { super(id, url); } /** @docsNotRequired */ override toString(): string { - return `RoutesRecognized(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${ - this.urlAfterRedirects}', state: ${this.state})`; + return `RoutesRecognized(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`; } } @@ -353,20 +358,20 @@ export class GuardsCheckStart extends RouterEvent { readonly type = EventType.GuardsCheckStart; constructor( - /** @docsNotRequired */ - id: number, - /** @docsNotRequired */ - url: string, - /** @docsNotRequired */ - public urlAfterRedirects: string, - /** @docsNotRequired */ - public state: RouterStateSnapshot) { + /** @docsNotRequired */ + id: number, + /** @docsNotRequired */ + url: string, + /** @docsNotRequired */ + public urlAfterRedirects: string, + /** @docsNotRequired */ + public state: RouterStateSnapshot, + ) { super(id, url); } override toString(): string { - return `GuardsCheckStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${ - this.urlAfterRedirects}', state: ${this.state})`; + return `GuardsCheckStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`; } } @@ -381,22 +386,22 @@ export class GuardsCheckEnd extends RouterEvent { readonly type = EventType.GuardsCheckEnd; constructor( - /** @docsNotRequired */ - id: number, - /** @docsNotRequired */ - url: string, - /** @docsNotRequired */ - public urlAfterRedirects: string, - /** @docsNotRequired */ - public state: RouterStateSnapshot, - /** @docsNotRequired */ - public shouldActivate: boolean) { + /** @docsNotRequired */ + id: number, + /** @docsNotRequired */ + url: string, + /** @docsNotRequired */ + public urlAfterRedirects: string, + /** @docsNotRequired */ + public state: RouterStateSnapshot, + /** @docsNotRequired */ + public shouldActivate: boolean, + ) { super(id, url); } override toString(): string { - return `GuardsCheckEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${ - this.urlAfterRedirects}', state: ${this.state}, shouldActivate: ${this.shouldActivate})`; + return `GuardsCheckEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state}, shouldActivate: ${this.shouldActivate})`; } } @@ -414,20 +419,20 @@ export class ResolveStart extends RouterEvent { readonly type = EventType.ResolveStart; constructor( - /** @docsNotRequired */ - id: number, - /** @docsNotRequired */ - url: string, - /** @docsNotRequired */ - public urlAfterRedirects: string, - /** @docsNotRequired */ - public state: RouterStateSnapshot) { + /** @docsNotRequired */ + id: number, + /** @docsNotRequired */ + url: string, + /** @docsNotRequired */ + public urlAfterRedirects: string, + /** @docsNotRequired */ + public state: RouterStateSnapshot, + ) { super(id, url); } override toString(): string { - return `ResolveStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${ - this.urlAfterRedirects}', state: ${this.state})`; + return `ResolveStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`; } } @@ -441,20 +446,20 @@ export class ResolveEnd extends RouterEvent { readonly type = EventType.ResolveEnd; constructor( - /** @docsNotRequired */ - id: number, - /** @docsNotRequired */ - url: string, - /** @docsNotRequired */ - public urlAfterRedirects: string, - /** @docsNotRequired */ - public state: RouterStateSnapshot) { + /** @docsNotRequired */ + id: number, + /** @docsNotRequired */ + url: string, + /** @docsNotRequired */ + public urlAfterRedirects: string, + /** @docsNotRequired */ + public state: RouterStateSnapshot, + ) { super(id, url); } override toString(): string { - return `ResolveEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${ - this.urlAfterRedirects}', state: ${this.state})`; + return `ResolveEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`; } } @@ -469,8 +474,9 @@ export class RouteConfigLoadStart { readonly type = EventType.RouteConfigLoadStart; constructor( - /** @docsNotRequired */ - public route: Route) {} + /** @docsNotRequired */ + public route: Route, + ) {} toString(): string { return `RouteConfigLoadStart(path: ${this.route.path})`; } @@ -487,8 +493,9 @@ export class RouteConfigLoadEnd { readonly type = EventType.RouteConfigLoadEnd; constructor( - /** @docsNotRequired */ - public route: Route) {} + /** @docsNotRequired */ + public route: Route, + ) {} toString(): string { return `RouteConfigLoadEnd(path: ${this.route.path})`; } @@ -506,10 +513,11 @@ export class ChildActivationStart { readonly type = EventType.ChildActivationStart; constructor( - /** @docsNotRequired */ - public snapshot: ActivatedRouteSnapshot) {} + /** @docsNotRequired */ + public snapshot: ActivatedRouteSnapshot, + ) {} toString(): string { - const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || ''; + const path = (this.snapshot.routeConfig && this.snapshot.routeConfig.path) || ''; return `ChildActivationStart(path: '${path}')`; } } @@ -525,10 +533,11 @@ export class ChildActivationEnd { readonly type = EventType.ChildActivationEnd; constructor( - /** @docsNotRequired */ - public snapshot: ActivatedRouteSnapshot) {} + /** @docsNotRequired */ + public snapshot: ActivatedRouteSnapshot, + ) {} toString(): string { - const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || ''; + const path = (this.snapshot.routeConfig && this.snapshot.routeConfig.path) || ''; return `ChildActivationEnd(path: '${path}')`; } } @@ -545,10 +554,11 @@ export class ActivationStart { readonly type = EventType.ActivationStart; constructor( - /** @docsNotRequired */ - public snapshot: ActivatedRouteSnapshot) {} + /** @docsNotRequired */ + public snapshot: ActivatedRouteSnapshot, + ) {} toString(): string { - const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || ''; + const path = (this.snapshot.routeConfig && this.snapshot.routeConfig.path) || ''; return `ActivationStart(path: '${path}')`; } } @@ -565,10 +575,11 @@ export class ActivationEnd { readonly type = EventType.ActivationEnd; constructor( - /** @docsNotRequired */ - public snapshot: ActivatedRouteSnapshot) {} + /** @docsNotRequired */ + public snapshot: ActivatedRouteSnapshot, + ) {} toString(): string { - const path = this.snapshot.routeConfig && this.snapshot.routeConfig.path || ''; + const path = (this.snapshot.routeConfig && this.snapshot.routeConfig.path) || ''; return `ActivationEnd(path: '${path}')`; } } @@ -582,14 +593,15 @@ export class Scroll { readonly type = EventType.Scroll; constructor( - /** @docsNotRequired */ - readonly routerEvent: NavigationEnd|NavigationSkipped, + /** @docsNotRequired */ + readonly routerEvent: NavigationEnd | NavigationSkipped, - /** @docsNotRequired */ - readonly position: [number, number]|null, + /** @docsNotRequired */ + readonly position: [number, number] | null, - /** @docsNotRequired */ - readonly anchor: string|null) {} + /** @docsNotRequired */ + readonly anchor: string | null, + ) {} toString(): string { const pos = this.position ? `${this.position[0]}, ${this.position[1]}` : null; @@ -601,7 +613,7 @@ export class BeforeActivateRoutes {} export class RedirectRequest { constructor(readonly url: UrlTree) {} } -export type PrivateRouterEvents = BeforeActivateRoutes|RedirectRequest; +export type PrivateRouterEvents = BeforeActivateRoutes | RedirectRequest; /** * Router events that allow you to track the lifecycle of the router. @@ -636,10 +648,24 @@ export type PrivateRouterEvents = BeforeActivateRoutes|RedirectRequest; * * @publicApi */ -export type Event = NavigationStart|NavigationEnd|NavigationCancel|NavigationError|RoutesRecognized| - GuardsCheckStart|GuardsCheckEnd|RouteConfigLoadStart|RouteConfigLoadEnd|ChildActivationStart| - ChildActivationEnd|ActivationStart|ActivationEnd|Scroll|ResolveStart|ResolveEnd| - NavigationSkipped; +export type Event = + | NavigationStart + | NavigationEnd + | NavigationCancel + | NavigationError + | RoutesRecognized + | GuardsCheckStart + | GuardsCheckEnd + | RouteConfigLoadStart + | RouteConfigLoadEnd + | ChildActivationStart + | ChildActivationEnd + | ActivationStart + | ActivationEnd + | Scroll + | ResolveStart + | ResolveEnd + | NavigationSkipped; export function stringifyEvent(routerEvent: Event): string { switch (routerEvent.type) { @@ -652,42 +678,33 @@ export function stringifyEvent(routerEvent: Event): string { case EventType.ChildActivationStart: return `ChildActivationStart(path: '${routerEvent.snapshot.routeConfig?.path || ''}')`; case EventType.GuardsCheckEnd: - return `GuardsCheckEnd(id: ${routerEvent.id}, url: '${ - routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${ - routerEvent.state}, shouldActivate: ${routerEvent.shouldActivate})`; + return `GuardsCheckEnd(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${routerEvent.state}, shouldActivate: ${routerEvent.shouldActivate})`; case EventType.GuardsCheckStart: - return `GuardsCheckStart(id: ${routerEvent.id}, url: '${ - routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${ - routerEvent.state})`; + return `GuardsCheckStart(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`; case EventType.NavigationCancel: return `NavigationCancel(id: ${routerEvent.id}, url: '${routerEvent.url}')`; case EventType.NavigationSkipped: return `NavigationSkipped(id: ${routerEvent.id}, url: '${routerEvent.url}')`; case EventType.NavigationEnd: - return `NavigationEnd(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${ - routerEvent.urlAfterRedirects}')`; + return `NavigationEnd(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}')`; case EventType.NavigationError: - return `NavigationError(id: ${routerEvent.id}, url: '${routerEvent.url}', error: ${ - routerEvent.error})`; + return `NavigationError(id: ${routerEvent.id}, url: '${routerEvent.url}', error: ${routerEvent.error})`; case EventType.NavigationStart: return `NavigationStart(id: ${routerEvent.id}, url: '${routerEvent.url}')`; case EventType.ResolveEnd: - return `ResolveEnd(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${ - routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`; + return `ResolveEnd(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`; case EventType.ResolveStart: - return `ResolveStart(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${ - routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`; + return `ResolveStart(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`; case EventType.RouteConfigLoadEnd: return `RouteConfigLoadEnd(path: ${routerEvent.route.path})`; case EventType.RouteConfigLoadStart: return `RouteConfigLoadStart(path: ${routerEvent.route.path})`; case EventType.RoutesRecognized: - return `RoutesRecognized(id: ${routerEvent.id}, url: '${ - routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${ - routerEvent.state})`; + return `RoutesRecognized(id: ${routerEvent.id}, url: '${routerEvent.url}', urlAfterRedirects: '${routerEvent.urlAfterRedirects}', state: ${routerEvent.state})`; case EventType.Scroll: - const pos = - routerEvent.position ? `${routerEvent.position[0]}, ${routerEvent.position[1]}` : null; + const pos = routerEvent.position + ? `${routerEvent.position[0]}, ${routerEvent.position[1]}` + : null; return `Scroll(anchor: '${routerEvent.anchor}', position: '${pos}')`; } } diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index 67c31484279..9390f76e0b5 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -6,31 +6,131 @@ * found in the LICENSE file at https://angular.io/license */ - export {createUrlTreeFromSnapshot} from './create_url_tree'; export {RouterLink, RouterLinkWithHref} from './directives/router_link'; export {RouterLinkActive} from './directives/router_link_active'; export {RouterOutlet, RouterOutletContract} from './directives/router_outlet'; -export {ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, Event, EventType, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationCancellationCode as NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationSkippedCode, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, RoutesRecognized, Scroll} from './events'; -export {CanActivateChildFn, CanActivateFn, CanDeactivateFn, CanLoadFn, CanMatchFn, Data, DefaultExport, LoadChildren, LoadChildrenCallback, NavigationBehaviorOptions, OnSameUrlNavigation, QueryParamsHandling, ResolveData, ResolveFn, Route, Routes, RunGuardsAndResolvers, UrlMatcher, UrlMatchResult} from './models'; +export { + ActivationEnd, + ActivationStart, + ChildActivationEnd, + ChildActivationStart, + Event, + EventType, + GuardsCheckEnd, + GuardsCheckStart, + NavigationCancel, + NavigationCancellationCode as NavigationCancellationCode, + NavigationEnd, + NavigationError, + NavigationSkipped, + NavigationSkippedCode, + NavigationStart, + ResolveEnd, + ResolveStart, + RouteConfigLoadEnd, + RouteConfigLoadStart, + RouterEvent, + RoutesRecognized, + Scroll, +} from './events'; +export { + CanActivateChildFn, + CanActivateFn, + CanDeactivateFn, + CanLoadFn, + CanMatchFn, + Data, + DefaultExport, + LoadChildren, + LoadChildrenCallback, + NavigationBehaviorOptions, + OnSameUrlNavigation, + QueryParamsHandling, + ResolveData, + ResolveFn, + Route, + Routes, + RunGuardsAndResolvers, + UrlMatcher, + UrlMatchResult, +} from './models'; export {ViewTransitionInfo, ViewTransitionsFeatureOptions} from './utils/view_transition'; export * from './models_deprecated'; export {Navigation, NavigationExtras, UrlCreationOptions} from './navigation_transition'; export {DefaultTitleStrategy, TitleStrategy} from './page_title_strategy'; -export {DebugTracingFeature, DisabledInitialNavigationFeature, withViewTransitions, ViewTransitionsFeature, EnabledBlockingInitialNavigationFeature, InitialNavigationFeature, InMemoryScrollingFeature, NavigationErrorHandlerFeature, PreloadingFeature, provideRouter, provideRoutes, RouterConfigurationFeature, RouterFeature, RouterFeatures, RouterHashLocationFeature, withComponentInputBinding, withDebugTracing, withDisabledInitialNavigation, withEnabledBlockingInitialNavigation, withHashLocation, withInMemoryScrolling, withNavigationErrorHandler, withPreloading, withRouterConfig} from './provide_router'; -export {BaseRouteReuseStrategy, DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy'; +export { + DebugTracingFeature, + DisabledInitialNavigationFeature, + withViewTransitions, + ViewTransitionsFeature, + EnabledBlockingInitialNavigationFeature, + InitialNavigationFeature, + InMemoryScrollingFeature, + NavigationErrorHandlerFeature, + PreloadingFeature, + provideRouter, + provideRoutes, + RouterConfigurationFeature, + RouterFeature, + RouterFeatures, + RouterHashLocationFeature, + withComponentInputBinding, + withDebugTracing, + withDisabledInitialNavigation, + withEnabledBlockingInitialNavigation, + withHashLocation, + withInMemoryScrolling, + withNavigationErrorHandler, + withPreloading, + withRouterConfig, +} from './provide_router'; +export { + BaseRouteReuseStrategy, + DetachedRouteHandle, + RouteReuseStrategy, +} from './route_reuse_strategy'; export {Router} from './router'; -export {ExtraOptions, InitialNavigation, InMemoryScrollingOptions, ROUTER_CONFIGURATION, RouterConfigOptions} from './router_config'; +export { + ExtraOptions, + InitialNavigation, + InMemoryScrollingOptions, + ROUTER_CONFIGURATION, + RouterConfigOptions, +} from './router_config'; export {ROUTES} from './router_config_loader'; export {ROUTER_INITIALIZER, RouterModule} from './router_module'; export {ChildrenOutletContexts, OutletContext} from './router_outlet_context'; -export {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader'; -export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state'; +export { + NoPreloading, + PreloadAllModules, + PreloadingStrategy, + RouterPreloader, +} from './router_preloader'; +export { + ActivatedRoute, + ActivatedRouteSnapshot, + RouterState, + RouterStateSnapshot, +} from './router_state'; export {convertToParamMap, defaultUrlMatcher, ParamMap, Params, PRIMARY_OUTLET} from './shared'; export {UrlHandlingStrategy} from './url_handling_strategy'; -export {DefaultUrlSerializer, IsActiveMatchOptions, UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree'; -export {mapToCanActivate, mapToCanActivateChild, mapToCanDeactivate, mapToCanMatch, mapToResolve} from './utils/functional_guards'; +export { + DefaultUrlSerializer, + IsActiveMatchOptions, + UrlSegment, + UrlSegmentGroup, + UrlSerializer, + UrlTree, +} from './url_tree'; +export { + mapToCanActivate, + mapToCanActivateChild, + mapToCanDeactivate, + mapToCanMatch, + mapToResolve, +} from './utils/functional_guards'; export {VERSION} from './version'; export * from './private_export'; diff --git a/packages/router/src/models.ts b/packages/router/src/models.ts index 14c039054dd..afe1b7cbe57 100644 --- a/packages/router/src/models.ts +++ b/packages/router/src/models.ts @@ -6,7 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {EnvironmentInjector, EnvironmentProviders, NgModuleFactory, Provider, ProviderToken, Type} from '@angular/core'; +import { + EnvironmentInjector, + EnvironmentProviders, + NgModuleFactory, + Provider, + ProviderToken, + Type, +} from '@angular/core'; import {Observable} from 'rxjs'; import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state'; @@ -36,7 +43,7 @@ import {UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree'; * @see {@link NavigationBehaviorOptions} * @see {@link RouterConfigOptions} */ -export type OnSameUrlNavigation = 'reload'|'ignore'; +export type OnSameUrlNavigation = 'reload' | 'ignore'; /** * The `InjectionToken` and `@Injectable` classes for guards and resolvers are deprecated in favor @@ -55,7 +62,7 @@ export type OnSameUrlNavigation = 'reload'|'ignore'; * @see {@link core/inject} * @publicApi */ -export type DeprecatedGuard = ProviderToken|any; +export type DeprecatedGuard = ProviderToken | any; /** * Represents a route configuration for the Router service. @@ -105,8 +112,11 @@ export type UrlMatchResult = { * * @publicApi */ -export type UrlMatcher = (segments: UrlSegment[], group: UrlSegmentGroup, route: Route) => - UrlMatchResult|null; +export type UrlMatcher = ( + segments: UrlSegment[], + group: UrlSegmentGroup, + route: Route, +) => UrlMatchResult | null; /** * @@ -117,7 +127,7 @@ export type UrlMatcher = (segments: UrlSegment[], group: UrlSegmentGroup, route: * @publicApi */ export type Data = { - [key: string|symbol]: any + [key: string | symbol]: any; }; /** @@ -129,7 +139,7 @@ export type Data = { * @publicApi */ export type ResolveData = { - [key: string|symbol]: ResolveFn|DeprecatedGuard + [key: string | symbol]: ResolveFn | DeprecatedGuard; }; /** @@ -183,9 +193,14 @@ export interface DefaultExport { * @see {@link Route#loadChildren} * @publicApi */ -export type LoadChildrenCallback = () => Type|NgModuleFactory|Routes| - Observable|Routes|DefaultExport>|DefaultExport>| - Promise|Type|Routes|DefaultExport>|DefaultExport>; +export type LoadChildrenCallback = () => + | Type + | NgModuleFactory + | Routes + | Observable | Routes | DefaultExport> | DefaultExport> + | Promise< + NgModuleFactory | Type | Routes | DefaultExport> | DefaultExport + >; /** * @@ -208,7 +223,7 @@ export type LoadChildren = LoadChildrenCallback; * @see {@link RouterLink} * @publicApi */ -export type QueryParamsHandling = 'merge'|'preserve'|''; +export type QueryParamsHandling = 'merge' | 'preserve' | ''; /** * A policy for when to run guards and resolvers on a route. @@ -229,8 +244,12 @@ export type QueryParamsHandling = 'merge'|'preserve'|''; * @publicApi */ export type RunGuardsAndResolvers = - 'pathParamsChange'|'pathParamsOrQueryParamsChange'|'paramsChange'|'paramsOrQueryParamsChange'| - 'always'|((from: ActivatedRouteSnapshot, to: ActivatedRouteSnapshot) => boolean); + | 'pathParamsChange' + | 'pathParamsOrQueryParamsChange' + | 'paramsChange' + | 'paramsOrQueryParamsChange' + | 'always' + | ((from: ActivatedRouteSnapshot, to: ActivatedRouteSnapshot) => boolean); /** * A configuration object that defines a single route. @@ -464,7 +483,7 @@ export interface Route { * * @see {@link TitleStrategy} */ - title?: string|Type>|ResolveFn; + title?: string | Type> | ResolveFn; /** * The path to match against. Cannot be used together with a custom `matcher` function. @@ -493,7 +512,7 @@ export interface Route { * to the redirect destination, creating an endless loop. * */ - pathMatch?: 'prefix'|'full'; + pathMatch?: 'prefix' | 'full'; /** * A custom URL-matching function. Cannot be used together with `path`. */ @@ -507,8 +526,10 @@ export interface Route { /** * An object specifying a lazy-loaded component. */ - loadComponent?: () => Type| Observable|DefaultExport>>| - Promise|DefaultExport>>; + loadComponent?: () => + | Type + | Observable | DefaultExport>> + | Promise | DefaultExport>>; /** * Filled for routes `loadComponent` once the component is loaded. * @internal @@ -536,7 +557,7 @@ export interface Route { * When using a function rather than DI tokens, the function can call `inject` to get any required * dependencies. This `inject` call must be done in a synchronous context. */ - canActivate?: Array; + canActivate?: Array; /** * An array of `CanMatchFn` or DI tokens used to look up `CanMatch()` * handlers, in order to determine if the current user is allowed to @@ -545,7 +566,7 @@ export interface Route { * When using a function rather than DI tokens, the function can call `inject` to get any required * dependencies. This `inject` call must be done in a synchronous context. */ - canMatch?: Array; + canMatch?: Array; /** * An array of `CanActivateChildFn` or DI tokens used to look up `CanActivateChild()` handlers, * in order to determine if the current user is allowed to activate @@ -554,7 +575,7 @@ export interface Route { * When using a function rather than DI tokens, the function can call `inject` to get any required * dependencies. This `inject` call must be done in a synchronous context. */ - canActivateChild?: Array; + canActivateChild?: Array; /** * An array of `CanDeactivateFn` or DI tokens used to look up `CanDeactivate()` * handlers, in order to determine if the current user is allowed to @@ -563,7 +584,7 @@ export interface Route { * When using a function rather than DI tokens, the function can call `inject` to get any required * dependencies. This `inject` call must be done in a synchronous context. */ - canDeactivate?: Array|DeprecatedGuard>; + canDeactivate?: Array | DeprecatedGuard>; /** * An array of `CanLoadFn` or DI tokens used to look up `CanLoad()` * handlers, in order to determine if the current user is allowed to @@ -573,7 +594,7 @@ export interface Route { * dependencies. This `inject` call must be done in a synchronous context. * @deprecated Use `canMatch` instead */ - canLoad?: Array; + canLoad?: Array; /** * Additional developer-defined data provided to the component via * `ActivatedRoute`. By default, no additional data is passed. @@ -620,7 +641,7 @@ export interface Route { * route also has a `loadChildren` function which returns an `NgModuleRef`, this injector will be * used as the parent of the lazy loaded module. */ - providers?: Array; + providers?: Array; /** * Injector created from the static route providers @@ -643,7 +664,7 @@ export interface Route { export interface LoadedRouterConfig { routes: Route[]; - injector: EnvironmentInjector|undefined; + injector: EnvironmentInjector | undefined; } /** @@ -704,8 +725,10 @@ export interface LoadedRouterConfig { * @see {@link CanActivateFn} */ export interface CanActivate { - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): - Observable|Promise|boolean|UrlTree; + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + ): Observable | Promise | boolean | UrlTree; } /** @@ -728,8 +751,10 @@ export interface CanActivate { * @publicApi * @see {@link Route} */ -export type CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => - Observable|Promise|boolean|UrlTree; +export type CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => Observable | Promise | boolean | UrlTree; /** * @description @@ -794,8 +819,10 @@ export type CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSn * @see {@link CanActivateChildFn} */ export interface CanActivateChild { - canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): - Observable|Promise|boolean|UrlTree; + canActivateChild( + childRoute: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + ): Observable | Promise | boolean | UrlTree; } /** @@ -813,8 +840,10 @@ export interface CanActivateChild { * @publicApi * @see {@link Route} */ -export type CanActivateChildFn = (childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot) => - Observable|Promise|boolean|UrlTree; +export type CanActivateChildFn = ( + childRoute: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => Observable | Promise | boolean | UrlTree; /** * @description @@ -878,9 +907,11 @@ export type CanActivateChildFn = (childRoute: ActivatedRouteSnapshot, state: Rou */ export interface CanDeactivate { canDeactivate( - component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, - nextState: RouterStateSnapshot): Observable|Promise|boolean - |UrlTree; + component: T, + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState: RouterStateSnapshot, + ): Observable | Promise | boolean | UrlTree; } /** @@ -898,10 +929,12 @@ export interface CanDeactivate { * @publicApi * @see {@link Route} */ -export type CanDeactivateFn = - (component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, - nextState: RouterStateSnapshot) => - Observable|Promise|boolean|UrlTree; +export type CanDeactivateFn = ( + component: T, + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState: RouterStateSnapshot, +) => Observable | Promise | boolean | UrlTree; /** * @description @@ -969,8 +1002,10 @@ export type CanDeactivateFn = * @see {@link CanMatchFn} */ export interface CanMatch { - canMatch(route: Route, segments: UrlSegment[]): - Observable|Promise|boolean|UrlTree; + canMatch( + route: Route, + segments: UrlSegment[], + ): Observable | Promise | boolean | UrlTree; } /** @@ -988,8 +1023,10 @@ export interface CanMatch { * @publicApi * @see {@link Route} */ -export type CanMatchFn = (route: Route, segments: UrlSegment[]) => - Observable|Promise|boolean|UrlTree; +export type CanMatchFn = ( + route: Route, + segments: UrlSegment[], +) => Observable | Promise | boolean | UrlTree; /** * @description @@ -1089,7 +1126,10 @@ export type CanMatchFn = (route: Route, segments: UrlSegment[]) => * @see {@link ResolveFn} */ export interface Resolve { - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable|Promise|T; + resolve( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + ): Observable | Promise | T; } /** @@ -1133,8 +1173,10 @@ export interface Resolve { * @publicApi * @see {@link Route} */ -export type ResolveFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => - Observable|Promise|T; +export type ResolveFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => Observable | Promise | T; /** * @description @@ -1191,8 +1233,10 @@ export type ResolveFn = (route: ActivatedRouteSnapshot, state: RouterStateSna * @deprecated Use {@link CanMatchFn} instead */ export interface CanLoad { - canLoad(route: Route, segments: UrlSegment[]): - Observable|Promise|boolean|UrlTree; + canLoad( + route: Route, + segments: UrlSegment[], + ): Observable | Promise | boolean | UrlTree; } /** @@ -1204,9 +1248,10 @@ export interface CanLoad { * @see {@link CanMatchFn} * @deprecated Use `Route.canMatch` and `CanMatchFn` instead */ -export type CanLoadFn = (route: Route, segments: UrlSegment[]) => - Observable|Promise|boolean|UrlTree; - +export type CanLoadFn = ( + route: Route, + segments: UrlSegment[], +) => Observable | Promise | boolean | UrlTree; /** * @description diff --git a/packages/router/src/models_deprecated.ts b/packages/router/src/models_deprecated.ts index 2d3de228eed..4130405c2c7 100644 --- a/packages/router/src/models_deprecated.ts +++ b/packages/router/src/models_deprecated.ts @@ -10,4 +10,12 @@ // The public API re-exports everything from this file, which can be patched // locally in g3 to prevent regressions after cleanups complete. -export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, CanMatch, DeprecatedGuard, Resolve} from './models'; +export { + CanActivate, + CanActivateChild, + CanDeactivate, + CanLoad, + CanMatch, + DeprecatedGuard, + Resolve, +} from './models'; diff --git a/packages/router/src/navigation_canceling_error.ts b/packages/router/src/navigation_canceling_error.ts index f82537dfa0e..c2ea490e89d 100644 --- a/packages/router/src/navigation_canceling_error.ts +++ b/packages/router/src/navigation_canceling_error.ts @@ -12,28 +12,36 @@ import {isUrlTree, UrlSerializer, UrlTree} from './url_tree'; export const NAVIGATION_CANCELING_ERROR = 'ngNavigationCancelingError'; -export type NavigationCancelingError = - Error&{[NAVIGATION_CANCELING_ERROR]: true, cancellationCode: NavigationCancellationCode}; -export type RedirectingNavigationCancelingError = NavigationCancelingError&{ +export type NavigationCancelingError = Error & { + [NAVIGATION_CANCELING_ERROR]: true; + cancellationCode: NavigationCancellationCode; +}; +export type RedirectingNavigationCancelingError = NavigationCancelingError & { url: UrlTree; navigationBehaviorOptions?: NavigationBehaviorOptions; cancellationCode: NavigationCancellationCode.Redirect; }; export function redirectingNavigationError( - urlSerializer: UrlSerializer, redirect: UrlTree): RedirectingNavigationCancelingError { - const {redirectTo, navigationBehaviorOptions} = - isUrlTree(redirect) ? {redirectTo: redirect, navigationBehaviorOptions: undefined} : redirect; + urlSerializer: UrlSerializer, + redirect: UrlTree, +): RedirectingNavigationCancelingError { + const {redirectTo, navigationBehaviorOptions} = isUrlTree(redirect) + ? {redirectTo: redirect, navigationBehaviorOptions: undefined} + : redirect; const error = navigationCancelingError( - ngDevMode && `Redirecting to "${urlSerializer.serialize(redirectTo)}"`, - NavigationCancellationCode.Redirect) as RedirectingNavigationCancelingError; + ngDevMode && `Redirecting to "${urlSerializer.serialize(redirectTo)}"`, + NavigationCancellationCode.Redirect, + ) as RedirectingNavigationCancelingError; error.url = redirectTo; error.navigationBehaviorOptions = navigationBehaviorOptions; return error; } export function navigationCancelingError( - message: string|null|false, code: NavigationCancellationCode) { + message: string | null | false, + code: NavigationCancellationCode, +) { const error = new Error(`NavigationCancelingError: ${message || ''}`) as NavigationCancelingError; error[NAVIGATION_CANCELING_ERROR] = true; error.cancellationCode = code; @@ -41,10 +49,12 @@ export function navigationCancelingError( } export function isRedirectingNavigationCancelingError( - error: unknown| - RedirectingNavigationCancelingError): error is RedirectingNavigationCancelingError { - return isNavigationCancelingError(error) && - isUrlTree((error as RedirectingNavigationCancelingError).url); + error: unknown | RedirectingNavigationCancelingError, +): error is RedirectingNavigationCancelingError { + return ( + isNavigationCancelingError(error) && + isUrlTree((error as RedirectingNavigationCancelingError).url) + ); } export function isNavigationCancelingError(error: unknown): error is NavigationCancelingError { diff --git a/packages/router/src/navigation_transition.ts b/packages/router/src/navigation_transition.ts index 705c5a6fdd8..313dabbe57b 100644 --- a/packages/router/src/navigation_transition.ts +++ b/packages/router/src/navigation_transition.ts @@ -9,13 +9,47 @@ import {Location} from '@angular/common'; import {EnvironmentInjector, inject, Injectable, Type} from '@angular/core'; import {BehaviorSubject, combineLatest, EMPTY, from, Observable, of, Subject} from 'rxjs'; -import {catchError, defaultIfEmpty, filter, finalize, map, switchMap, take, takeUntil, tap} from 'rxjs/operators'; +import { + catchError, + defaultIfEmpty, + filter, + finalize, + map, + switchMap, + take, + takeUntil, + tap, +} from 'rxjs/operators'; import {createRouterState} from './create_router_state'; import {INPUT_BINDER} from './directives/router_outlet'; -import {BeforeActivateRoutes, Event, GuardsCheckEnd, GuardsCheckStart, IMPERATIVE_NAVIGATION, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationSkippedCode, NavigationStart, NavigationTrigger, RedirectRequest, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events'; +import { + BeforeActivateRoutes, + Event, + GuardsCheckEnd, + GuardsCheckStart, + IMPERATIVE_NAVIGATION, + NavigationCancel, + NavigationCancellationCode, + NavigationEnd, + NavigationError, + NavigationSkipped, + NavigationSkippedCode, + NavigationStart, + NavigationTrigger, + RedirectRequest, + ResolveEnd, + ResolveStart, + RouteConfigLoadEnd, + RouteConfigLoadStart, + RoutesRecognized, +} from './events'; import {NavigationBehaviorOptions, QueryParamsHandling, Route, Routes} from './models'; -import {isNavigationCancelingError, isRedirectingNavigationCancelingError, redirectingNavigationError} from './navigation_canceling_error'; +import { + isNavigationCancelingError, + isRedirectingNavigationCancelingError, + redirectingNavigationError, +} from './navigation_canceling_error'; import {activateRoutes} from './operators/activate_routes'; import {checkGuards} from './operators/check_guards'; import {recognize} from './operators/recognize'; @@ -26,15 +60,19 @@ import {RouteReuseStrategy} from './route_reuse_strategy'; import {ROUTER_CONFIGURATION} from './router_config'; import {RouterConfigLoader} from './router_config_loader'; import {ChildrenOutletContexts} from './router_outlet_context'; -import {ActivatedRoute, ActivatedRouteSnapshot, createEmptyState, RouterState, RouterStateSnapshot} from './router_state'; +import { + ActivatedRoute, + ActivatedRouteSnapshot, + createEmptyState, + RouterState, + RouterStateSnapshot, +} from './router_state'; import {Params} from './shared'; import {UrlHandlingStrategy} from './url_handling_strategy'; import {isUrlTree, UrlSerializer, UrlTree} from './url_tree'; import {Checks, getAllRouteGuards} from './utils/preactivation'; import {CREATE_VIEW_TRANSITION} from './utils/view_transition'; - - /** * @description * @@ -86,7 +124,7 @@ export interface UrlCreationOptions { * A value of `null` or `undefined` indicates that the navigation commands should be applied * relative to the root. */ - relativeTo?: ActivatedRoute|null; + relativeTo?: ActivatedRoute | null; /** * Sets query parameters to the URL. @@ -96,7 +134,7 @@ export interface UrlCreationOptions { * router.navigate(['/results'], { queryParams: { page: 1 } }); * ``` */ - queryParams?: Params|null; + queryParams?: Params | null; /** * Sets the hash fragment for the URL. @@ -130,7 +168,7 @@ export interface UrlCreationOptions { * the new value is used. * */ - queryParamsHandling?: QueryParamsHandling|null; + queryParamsHandling?: QueryParamsHandling | null; /** * When true, preserves the URL fragment for the next navigation @@ -162,12 +200,12 @@ export interface UrlCreationOptions { export interface NavigationExtras extends UrlCreationOptions, NavigationBehaviorOptions {} export type RestoredState = { - [k: string]: any, + [k: string]: any; // TODO(#27607): Remove `navigationId` and `ɵrouterPageId` and move to `ng` or `ɵ` namespace. - navigationId: number, + navigationId: number; // The `ɵ` prefix is there to reduce the chance of colliding with any existing user properties on // the history state. - ɵrouterPageId?: number, + ɵrouterPageId?: number; }; /** @@ -227,7 +265,7 @@ export interface Navigation { * * 'popstate'--Triggered by a popstate event. * * 'hashchange'--Triggered by a hashchange event. */ - trigger: 'imperative'|'popstate'|'hashchange'; + trigger: 'imperative' | 'popstate' | 'hashchange'; /** * Options that controlled the strategy used for this navigation. * See `NavigationExtras`. @@ -238,7 +276,7 @@ export interface Navigation { * is available, therefore this previous `Navigation` object has a `null` value * for its own `previousNavigation`. */ - previousNavigation: Navigation|null; + previousNavigation: Navigation | null; } export interface NavigationTransition { @@ -253,13 +291,13 @@ export interface NavigationTransition { reject: any; promise: Promise; source: NavigationTrigger; - restoredState: RestoredState|null; + restoredState: RestoredState | null; currentSnapshot: RouterStateSnapshot; - targetSnapshot: RouterStateSnapshot|null; + targetSnapshot: RouterStateSnapshot | null; currentRouterState: RouterState; - targetRouterState: RouterState|null; + targetRouterState: RouterState | null; guards: Checks; - guardsResult: boolean|UrlTree|null; + guardsResult: boolean | UrlTree | null; } /** @@ -276,20 +314,20 @@ interface InternalRouterInterface { errorHandler: (error: any) => any; navigated: boolean; routeReuseStrategy: RouteReuseStrategy; - onSameUrlNavigation: 'reload'|'ignore'; + onSameUrlNavigation: 'reload' | 'ignore'; } @Injectable({providedIn: 'root'}) export class NavigationTransitions { - currentNavigation: Navigation|null = null; - currentTransition: NavigationTransition|null = null; - lastSuccessfulNavigation: Navigation|null = null; + currentNavigation: Navigation | null = null; + currentTransition: NavigationTransition | null = null; + lastSuccessfulNavigation: Navigation | null = null; /** * These events are used to communicate back to the Router about the state of the transition. The * Router wants to respond to these events in various ways. Because the `NavigationTransition` * class is not public, this event subject is not publicly exposed. */ - readonly events = new Subject(); + readonly events = new Subject(); /** * Used to abort the current transition with an error. */ @@ -303,7 +341,7 @@ export class NavigationTransitions { private readonly titleStrategy?: TitleStrategy = inject(TitleStrategy); private readonly options = inject(ROUTER_CONFIGURATION, {optional: true}) || {}; private readonly paramsInheritanceStrategy = - this.options.paramsInheritanceStrategy || 'emptyOnly'; + this.options.paramsInheritanceStrategy || 'emptyOnly'; private readonly urlHandlingStrategy = inject(UrlHandlingStrategy); private readonly createViewTransition = inject(CREATE_VIEW_TRANSITION, {optional: true}); @@ -320,7 +358,7 @@ export class NavigationTransitions { */ afterPreactivation: () => Observable = () => of(void 0); /** @internal */ - rootComponentType: Type|null = null; + rootComponentType: Type | null = null; constructor() { const onLoadStart = (r: Route) => this.events.next(new RouteConfigLoadStart(r)); @@ -334,17 +372,30 @@ export class NavigationTransitions { } handleNavigationRequest( - request: Pick< - NavigationTransition, - 'source'|'restoredState'|'currentUrlTree'|'currentRawUrl'|'rawUrl'|'extras'|'resolve'| - 'reject'|'promise'|'currentSnapshot'|'currentRouterState'>) { + request: Pick< + NavigationTransition, + | 'source' + | 'restoredState' + | 'currentUrlTree' + | 'currentRawUrl' + | 'rawUrl' + | 'extras' + | 'resolve' + | 'reject' + | 'promise' + | 'currentSnapshot' + | 'currentRouterState' + >, + ) { const id = ++this.navigationId; this.transitions?.next({...this.transitions.value, ...request, id}); } setupNavigations( - router: InternalRouterInterface, initialUrlTree: UrlTree, - initialRouterState: RouterState): Observable { + router: InternalRouterInterface, + initialUrlTree: UrlTree, + initialRouterState: RouterState, + ): Observable { this.transitions = new BehaviorSubject({ id: 0, currentUrlTree: initialUrlTree, @@ -366,384 +417,450 @@ export class NavigationTransitions { guardsResult: null, }); return this.transitions.pipe( - filter(t => t.id !== 0), + filter((t) => t.id !== 0), - // Extract URL - map(t => - ({...t, extractedUrl: this.urlHandlingStrategy.extract(t.rawUrl)} as - NavigationTransition)), + // Extract URL + map( + (t) => + ({ + ...t, + extractedUrl: this.urlHandlingStrategy.extract(t.rawUrl), + }) as NavigationTransition, + ), - // Using switchMap so we cancel executing navigations when a new one comes in - switchMap(overallTransitionState => { - this.currentTransition = overallTransitionState; - let completed = false; - let errored = false; - return of(overallTransitionState) - .pipe( - // Store the Navigation object - tap(t => { - this.currentNavigation = { - id: t.id, - initialUrl: t.rawUrl, - extractedUrl: t.extractedUrl, - trigger: t.source, - extras: t.extras, - previousNavigation: !this.lastSuccessfulNavigation ? null : { - ...this.lastSuccessfulNavigation, - previousNavigation: null, - }, - }; - }), - switchMap(t => { - const urlTransition = !router.navigated || - this.isUpdatingInternalState() || this.isUpdatedBrowserUrl(); + // Using switchMap so we cancel executing navigations when a new one comes in + switchMap((overallTransitionState) => { + this.currentTransition = overallTransitionState; + let completed = false; + let errored = false; + return of(overallTransitionState).pipe( + // Store the Navigation object + tap((t) => { + this.currentNavigation = { + id: t.id, + initialUrl: t.rawUrl, + extractedUrl: t.extractedUrl, + trigger: t.source, + extras: t.extras, + previousNavigation: !this.lastSuccessfulNavigation + ? null + : { + ...this.lastSuccessfulNavigation, + previousNavigation: null, + }, + }; + }), + switchMap((t) => { + const urlTransition = + !router.navigated || this.isUpdatingInternalState() || this.isUpdatedBrowserUrl(); - const onSameUrlNavigation = - t.extras.onSameUrlNavigation ?? router.onSameUrlNavigation; - if (!urlTransition && onSameUrlNavigation !== 'reload') { - const reason = (typeof ngDevMode === 'undefined' || ngDevMode) ? - `Navigation to ${ - t.rawUrl} was ignored because it is the same as the current Router URL.` : - ''; - this.events.next(new NavigationSkipped( - t.id, this.urlSerializer.serialize(t.rawUrl), reason, - NavigationSkippedCode.IgnoredSameUrlNavigation)); - t.resolve(null); - return EMPTY; - } + const onSameUrlNavigation = t.extras.onSameUrlNavigation ?? router.onSameUrlNavigation; + if (!urlTransition && onSameUrlNavigation !== 'reload') { + const reason = + typeof ngDevMode === 'undefined' || ngDevMode + ? `Navigation to ${t.rawUrl} was ignored because it is the same as the current Router URL.` + : ''; + this.events.next( + new NavigationSkipped( + t.id, + this.urlSerializer.serialize(t.rawUrl), + reason, + NavigationSkippedCode.IgnoredSameUrlNavigation, + ), + ); + t.resolve(null); + return EMPTY; + } - if (this.urlHandlingStrategy.shouldProcessUrl(t.rawUrl)) { - return of(t).pipe( - // Fire NavigationStart event - switchMap(t => { - const transition = this.transitions?.getValue(); - this.events.next(new NavigationStart( - t.id, this.urlSerializer.serialize(t.extractedUrl), t.source, - t.restoredState)); - if (transition !== this.transitions?.getValue()) { - return EMPTY; - } + if (this.urlHandlingStrategy.shouldProcessUrl(t.rawUrl)) { + return of(t).pipe( + // Fire NavigationStart event + switchMap((t) => { + const transition = this.transitions?.getValue(); + this.events.next( + new NavigationStart( + t.id, + this.urlSerializer.serialize(t.extractedUrl), + t.source, + t.restoredState, + ), + ); + if (transition !== this.transitions?.getValue()) { + return EMPTY; + } - // This delay is required to match old behavior that forced - // navigation to always be async - return Promise.resolve(t); - }), + // This delay is required to match old behavior that forced + // navigation to always be async + return Promise.resolve(t); + }), - // Recognize - recognize( - this.environmentInjector, this.configLoader, - this.rootComponentType, router.config, this.urlSerializer, - this.paramsInheritanceStrategy), + // Recognize + recognize( + this.environmentInjector, + this.configLoader, + this.rootComponentType, + router.config, + this.urlSerializer, + this.paramsInheritanceStrategy, + ), - // Update URL if in `eager` update mode - tap(t => { - overallTransitionState.targetSnapshot = t.targetSnapshot; - overallTransitionState.urlAfterRedirects = t.urlAfterRedirects; - this.currentNavigation = { - ...this.currentNavigation!, - finalUrl: t.urlAfterRedirects - }; + // Update URL if in `eager` update mode + tap((t) => { + overallTransitionState.targetSnapshot = t.targetSnapshot; + overallTransitionState.urlAfterRedirects = t.urlAfterRedirects; + this.currentNavigation = { + ...this.currentNavigation!, + finalUrl: t.urlAfterRedirects, + }; - // Fire RoutesRecognized - const routesRecognized = new RoutesRecognized( - t.id, this.urlSerializer.serialize(t.extractedUrl), - this.urlSerializer.serialize(t.urlAfterRedirects!), - t.targetSnapshot!); - this.events.next(routesRecognized); - })); - } else if ( - urlTransition && - this.urlHandlingStrategy.shouldProcessUrl(t.currentRawUrl)) { - /* When the current URL shouldn't be processed, but the previous one - * was, we handle this "error condition" by navigating to the - * previously successful URL, but leaving the URL intact.*/ - const {id, extractedUrl, source, restoredState, extras} = t; - const navStart = new NavigationStart( - id, this.urlSerializer.serialize(extractedUrl), source, - restoredState); - this.events.next(navStart); - const targetSnapshot = - createEmptyState(this.rootComponentType).snapshot; + // Fire RoutesRecognized + const routesRecognized = new RoutesRecognized( + t.id, + this.urlSerializer.serialize(t.extractedUrl), + this.urlSerializer.serialize(t.urlAfterRedirects!), + t.targetSnapshot!, + ); + this.events.next(routesRecognized); + }), + ); + } else if ( + urlTransition && + this.urlHandlingStrategy.shouldProcessUrl(t.currentRawUrl) + ) { + /* When the current URL shouldn't be processed, but the previous one + * was, we handle this "error condition" by navigating to the + * previously successful URL, but leaving the URL intact.*/ + const {id, extractedUrl, source, restoredState, extras} = t; + const navStart = new NavigationStart( + id, + this.urlSerializer.serialize(extractedUrl), + source, + restoredState, + ); + this.events.next(navStart); + const targetSnapshot = createEmptyState(this.rootComponentType).snapshot; - this.currentTransition = overallTransitionState = { - ...t, - targetSnapshot, - urlAfterRedirects: extractedUrl, - extras: {...extras, skipLocationChange: false, replaceUrl: false}, - }; - this.currentNavigation!.finalUrl = extractedUrl; - return of(overallTransitionState); - } else { - /* When neither the current or previous URL can be processed, do - * nothing other than update router's internal reference to the - * current "settled" URL. This way the next navigation will be coming - * from the current URL in the browser. - */ - const reason = (typeof ngDevMode === 'undefined' || ngDevMode) ? - `Navigation was ignored because the UrlHandlingStrategy` + - ` indicated neither the current URL ${ - t.currentRawUrl} nor target URL ${ - t.rawUrl} should be processed.` : - ''; - this.events.next(new NavigationSkipped( - t.id, this.urlSerializer.serialize(t.extractedUrl), reason, - NavigationSkippedCode.IgnoredByUrlHandlingStrategy)); - t.resolve(null); - return EMPTY; - } - }), + this.currentTransition = overallTransitionState = { + ...t, + targetSnapshot, + urlAfterRedirects: extractedUrl, + extras: {...extras, skipLocationChange: false, replaceUrl: false}, + }; + this.currentNavigation!.finalUrl = extractedUrl; + return of(overallTransitionState); + } else { + /* When neither the current or previous URL can be processed, do + * nothing other than update router's internal reference to the + * current "settled" URL. This way the next navigation will be coming + * from the current URL in the browser. + */ + const reason = + typeof ngDevMode === 'undefined' || ngDevMode + ? `Navigation was ignored because the UrlHandlingStrategy` + + ` indicated neither the current URL ${t.currentRawUrl} nor target URL ${t.rawUrl} should be processed.` + : ''; + this.events.next( + new NavigationSkipped( + t.id, + this.urlSerializer.serialize(t.extractedUrl), + reason, + NavigationSkippedCode.IgnoredByUrlHandlingStrategy, + ), + ); + t.resolve(null); + return EMPTY; + } + }), - // --- GUARDS --- - tap(t => { - const guardsStart = new GuardsCheckStart( - t.id, this.urlSerializer.serialize(t.extractedUrl), - this.urlSerializer.serialize(t.urlAfterRedirects!), - t.targetSnapshot!); - this.events.next(guardsStart); - }), + // --- GUARDS --- + tap((t) => { + const guardsStart = new GuardsCheckStart( + t.id, + this.urlSerializer.serialize(t.extractedUrl), + this.urlSerializer.serialize(t.urlAfterRedirects!), + t.targetSnapshot!, + ); + this.events.next(guardsStart); + }), - map(t => { - this.currentTransition = overallTransitionState = { - ...t, - guards: getAllRouteGuards( - t.targetSnapshot!, t.currentSnapshot, this.rootContexts) - }; - return overallTransitionState; - }), + map((t) => { + this.currentTransition = overallTransitionState = { + ...t, + guards: getAllRouteGuards(t.targetSnapshot!, t.currentSnapshot, this.rootContexts), + }; + return overallTransitionState; + }), - checkGuards( - this.environmentInjector, (evt: Event) => this.events.next(evt)), - tap(t => { - overallTransitionState.guardsResult = t.guardsResult; - if (isUrlTree(t.guardsResult)) { - throw redirectingNavigationError(this.urlSerializer, t.guardsResult); - } + checkGuards(this.environmentInjector, (evt: Event) => this.events.next(evt)), + tap((t) => { + overallTransitionState.guardsResult = t.guardsResult; + if (isUrlTree(t.guardsResult)) { + throw redirectingNavigationError(this.urlSerializer, t.guardsResult); + } - const guardsEnd = new GuardsCheckEnd( - t.id, this.urlSerializer.serialize(t.extractedUrl), - this.urlSerializer.serialize(t.urlAfterRedirects!), - t.targetSnapshot!, !!t.guardsResult); - this.events.next(guardsEnd); - }), + const guardsEnd = new GuardsCheckEnd( + t.id, + this.urlSerializer.serialize(t.extractedUrl), + this.urlSerializer.serialize(t.urlAfterRedirects!), + t.targetSnapshot!, + !!t.guardsResult, + ); + this.events.next(guardsEnd); + }), - filter(t => { - if (!t.guardsResult) { - this.cancelNavigationTransition( - t, '', NavigationCancellationCode.GuardRejected); - return false; - } - return true; - }), + filter((t) => { + if (!t.guardsResult) { + this.cancelNavigationTransition(t, '', NavigationCancellationCode.GuardRejected); + return false; + } + return true; + }), - // --- RESOLVE --- - switchTap(t => { - if (t.guards.canActivateChecks.length) { - return of(t).pipe( - tap(t => { - const resolveStart = new ResolveStart( - t.id, this.urlSerializer.serialize(t.extractedUrl), - this.urlSerializer.serialize(t.urlAfterRedirects!), - t.targetSnapshot!); - this.events.next(resolveStart); - }), - switchMap(t => { - let dataResolved = false; - return of(t).pipe( - resolveData( - this.paramsInheritanceStrategy, - this.environmentInjector), - tap({ - next: () => dataResolved = true, - complete: () => { - if (!dataResolved) { - this.cancelNavigationTransition( - t, - (typeof ngDevMode === 'undefined' || ngDevMode) ? - `At least one route resolver didn't emit any value.` : - '', - NavigationCancellationCode.NoDataFromResolver); - } - } - }), - ); - }), - tap(t => { - const resolveEnd = new ResolveEnd( - t.id, this.urlSerializer.serialize(t.extractedUrl), - this.urlSerializer.serialize(t.urlAfterRedirects!), - t.targetSnapshot!); - this.events.next(resolveEnd); - })); - } - return undefined; - }), + // --- RESOLVE --- + switchTap((t) => { + if (t.guards.canActivateChecks.length) { + return of(t).pipe( + tap((t) => { + const resolveStart = new ResolveStart( + t.id, + this.urlSerializer.serialize(t.extractedUrl), + this.urlSerializer.serialize(t.urlAfterRedirects!), + t.targetSnapshot!, + ); + this.events.next(resolveStart); + }), + switchMap((t) => { + let dataResolved = false; + return of(t).pipe( + resolveData(this.paramsInheritanceStrategy, this.environmentInjector), + tap({ + next: () => (dataResolved = true), + complete: () => { + if (!dataResolved) { + this.cancelNavigationTransition( + t, + typeof ngDevMode === 'undefined' || ngDevMode + ? `At least one route resolver didn't emit any value.` + : '', + NavigationCancellationCode.NoDataFromResolver, + ); + } + }, + }), + ); + }), + tap((t) => { + const resolveEnd = new ResolveEnd( + t.id, + this.urlSerializer.serialize(t.extractedUrl), + this.urlSerializer.serialize(t.urlAfterRedirects!), + t.targetSnapshot!, + ); + this.events.next(resolveEnd); + }), + ); + } + return undefined; + }), - // --- LOAD COMPONENTS --- - switchTap((t: NavigationTransition) => { - const loadComponents = - (route: ActivatedRouteSnapshot): Array> => { - const loaders: Array> = []; - if (route.routeConfig?.loadComponent && - !route.routeConfig._loadedComponent) { - loaders.push(this.configLoader.loadComponent(route.routeConfig) - .pipe( - tap(loadedComponent => { - route.component = loadedComponent; - }), - map(() => void 0), - )); - } - for (const child of route.children) { - loaders.push(...loadComponents(child)); - } - return loaders; - }; - return combineLatest(loadComponents(t.targetSnapshot!.root)) - .pipe(defaultIfEmpty(null), take(1)); - }), + // --- LOAD COMPONENTS --- + switchTap((t: NavigationTransition) => { + const loadComponents = (route: ActivatedRouteSnapshot): Array> => { + const loaders: Array> = []; + if (route.routeConfig?.loadComponent && !route.routeConfig._loadedComponent) { + loaders.push( + this.configLoader.loadComponent(route.routeConfig).pipe( + tap((loadedComponent) => { + route.component = loadedComponent; + }), + map(() => void 0), + ), + ); + } + for (const child of route.children) { + loaders.push(...loadComponents(child)); + } + return loaders; + }; + return combineLatest(loadComponents(t.targetSnapshot!.root)).pipe( + defaultIfEmpty(null), + take(1), + ); + }), - switchTap(() => this.afterPreactivation()), + switchTap(() => this.afterPreactivation()), - switchMap(() => { - const {currentSnapshot, targetSnapshot} = overallTransitionState; - const viewTransitionStarted = this.createViewTransition?.( - this.environmentInjector, currentSnapshot.root, - targetSnapshot!.root); + switchMap(() => { + const {currentSnapshot, targetSnapshot} = overallTransitionState; + const viewTransitionStarted = this.createViewTransition?.( + this.environmentInjector, + currentSnapshot.root, + targetSnapshot!.root, + ); - // If view transitions are enabled, block the navigation until the view - // transition callback starts. Otherwise, continue immediately. - return viewTransitionStarted ? - from(viewTransitionStarted).pipe(map(() => overallTransitionState)) : - of(overallTransitionState); - }), + // If view transitions are enabled, block the navigation until the view + // transition callback starts. Otherwise, continue immediately. + return viewTransitionStarted + ? from(viewTransitionStarted).pipe(map(() => overallTransitionState)) + : of(overallTransitionState); + }), - map((t: NavigationTransition) => { - const targetRouterState = createRouterState( - router.routeReuseStrategy, t.targetSnapshot!, t.currentRouterState); - this.currentTransition = - overallTransitionState = {...t, targetRouterState}; - this.currentNavigation!.targetRouterState = targetRouterState; - return overallTransitionState; - }), + map((t: NavigationTransition) => { + const targetRouterState = createRouterState( + router.routeReuseStrategy, + t.targetSnapshot!, + t.currentRouterState, + ); + this.currentTransition = overallTransitionState = {...t, targetRouterState}; + this.currentNavigation!.targetRouterState = targetRouterState; + return overallTransitionState; + }), - tap(() => { - this.events.next(new BeforeActivateRoutes()); - }), + tap(() => { + this.events.next(new BeforeActivateRoutes()); + }), - activateRoutes( - this.rootContexts, router.routeReuseStrategy, - (evt: Event) => this.events.next(evt), this.inputBindingEnabled), + activateRoutes( + this.rootContexts, + router.routeReuseStrategy, + (evt: Event) => this.events.next(evt), + this.inputBindingEnabled, + ), - // Ensure that if some observable used to drive the transition doesn't - // complete, the navigation still finalizes This should never happen, but - // this is done as a safety measure to avoid surfacing this error (#49567). - take(1), + // Ensure that if some observable used to drive the transition doesn't + // complete, the navigation still finalizes This should never happen, but + // this is done as a safety measure to avoid surfacing this error (#49567). + take(1), - tap({ - next: (t: NavigationTransition) => { - completed = true; - this.lastSuccessfulNavigation = this.currentNavigation; - this.events.next(new NavigationEnd( - t.id, this.urlSerializer.serialize(t.extractedUrl), - this.urlSerializer.serialize(t.urlAfterRedirects!))); - this.titleStrategy?.updateTitle(t.targetRouterState!.snapshot); - t.resolve(true); - }, - complete: () => { - completed = true; - } - }), + tap({ + next: (t: NavigationTransition) => { + completed = true; + this.lastSuccessfulNavigation = this.currentNavigation; + this.events.next( + new NavigationEnd( + t.id, + this.urlSerializer.serialize(t.extractedUrl), + this.urlSerializer.serialize(t.urlAfterRedirects!), + ), + ); + this.titleStrategy?.updateTitle(t.targetRouterState!.snapshot); + t.resolve(true); + }, + complete: () => { + completed = true; + }, + }), - // There used to be a lot more logic happening directly within the - // transition Observable. Some of this logic has been refactored out to - // other places but there may still be errors that happen there. This gives - // us a way to cancel the transition from the outside. This may also be - // required in the future to support something like the abort signal of the - // Navigation API where the navigation gets aborted from outside the - // transition. - takeUntil(this.transitionAbortSubject.pipe(tap(err => { - throw err; - }))), + // There used to be a lot more logic happening directly within the + // transition Observable. Some of this logic has been refactored out to + // other places but there may still be errors that happen there. This gives + // us a way to cancel the transition from the outside. This may also be + // required in the future to support something like the abort signal of the + // Navigation API where the navigation gets aborted from outside the + // transition. + takeUntil( + this.transitionAbortSubject.pipe( + tap((err) => { + throw err; + }), + ), + ), - finalize(() => { - /* When the navigation stream finishes either through error or success, - * we set the `completed` or `errored` flag. However, there are some - * situations where we could get here without either of those being set. - * For instance, a redirect during NavigationStart. Therefore, this is a - * catch-all to make sure the NavigationCancel event is fired when a - * navigation gets cancelled but not caught by other means. */ - if (!completed && !errored) { - const cancelationReason = - (typeof ngDevMode === 'undefined' || ngDevMode) ? - `Navigation ID ${ - overallTransitionState - .id} is not equal to the current navigation id ${ - this.navigationId}` : - ''; - this.cancelNavigationTransition( - overallTransitionState, cancelationReason, - NavigationCancellationCode.SupersededByNewNavigation); - } - // Only clear current navigation if it is still set to the one that - // finalized. - if (this.currentNavigation?.id === overallTransitionState.id) { - this.currentNavigation = null; - } - }), - catchError((e) => { - errored = true; - /* This error type is issued during Redirect, and is handled as a - * cancellation rather than an error. */ - if (isNavigationCancelingError(e)) { - this.events.next(new NavigationCancel( - overallTransitionState.id, - this.urlSerializer.serialize(overallTransitionState.extractedUrl), - e.message, e.cancellationCode)); + finalize(() => { + /* When the navigation stream finishes either through error or success, + * we set the `completed` or `errored` flag. However, there are some + * situations where we could get here without either of those being set. + * For instance, a redirect during NavigationStart. Therefore, this is a + * catch-all to make sure the NavigationCancel event is fired when a + * navigation gets cancelled but not caught by other means. */ + if (!completed && !errored) { + const cancelationReason = + typeof ngDevMode === 'undefined' || ngDevMode + ? `Navigation ID ${overallTransitionState.id} is not equal to the current navigation id ${this.navigationId}` + : ''; + this.cancelNavigationTransition( + overallTransitionState, + cancelationReason, + NavigationCancellationCode.SupersededByNewNavigation, + ); + } + // Only clear current navigation if it is still set to the one that + // finalized. + if (this.currentNavigation?.id === overallTransitionState.id) { + this.currentNavigation = null; + } + }), + catchError((e) => { + errored = true; + /* This error type is issued during Redirect, and is handled as a + * cancellation rather than an error. */ + if (isNavigationCancelingError(e)) { + this.events.next( + new NavigationCancel( + overallTransitionState.id, + this.urlSerializer.serialize(overallTransitionState.extractedUrl), + e.message, + e.cancellationCode, + ), + ); - // When redirecting, we need to delay resolving the navigation - // promise and push it to the redirect navigation - if (!isRedirectingNavigationCancelingError(e)) { - overallTransitionState.resolve(false); - } else { - this.events.next(new RedirectRequest(e.url)); - } + // When redirecting, we need to delay resolving the navigation + // promise and push it to the redirect navigation + if (!isRedirectingNavigationCancelingError(e)) { + overallTransitionState.resolve(false); + } else { + this.events.next(new RedirectRequest(e.url)); + } - /* All other errors should reset to the router's internal URL reference - * to the pre-error state. */ - } else { - this.events.next(new NavigationError( - overallTransitionState.id, - this.urlSerializer.serialize(overallTransitionState.extractedUrl), - e, overallTransitionState.targetSnapshot ?? undefined)); - try { - overallTransitionState.resolve(router.errorHandler(e)); - } catch (ee) { - // TODO(atscott): consider flipping the default behavior of - // resolveNavigationPromiseOnError to be `resolve(false)` when - // undefined. This is the most sane thing to do given that - // applications very rarely handle the promise rejection and, as a - // result, would get "unhandled promise rejection" console logs. - // The vast majority of applications would not be affected by this - // change so omitting a migration seems reasonable. Instead, - // applications that rely on rejection can specifically opt-in to the - // old behavior. - if (this.options.resolveNavigationPromiseOnError) { - overallTransitionState.resolve(false); - } else { - overallTransitionState.reject(ee); - } - } - } - return EMPTY; - })); - // casting because `pipe` returns observable({}) when called with 8+ arguments - })) as Observable; + /* All other errors should reset to the router's internal URL reference + * to the pre-error state. */ + } else { + this.events.next( + new NavigationError( + overallTransitionState.id, + this.urlSerializer.serialize(overallTransitionState.extractedUrl), + e, + overallTransitionState.targetSnapshot ?? undefined, + ), + ); + try { + overallTransitionState.resolve(router.errorHandler(e)); + } catch (ee) { + // TODO(atscott): consider flipping the default behavior of + // resolveNavigationPromiseOnError to be `resolve(false)` when + // undefined. This is the most sane thing to do given that + // applications very rarely handle the promise rejection and, as a + // result, would get "unhandled promise rejection" console logs. + // The vast majority of applications would not be affected by this + // change so omitting a migration seems reasonable. Instead, + // applications that rely on rejection can specifically opt-in to the + // old behavior. + if (this.options.resolveNavigationPromiseOnError) { + overallTransitionState.resolve(false); + } else { + overallTransitionState.reject(ee); + } + } + } + return EMPTY; + }), + ); + // casting because `pipe` returns observable({}) when called with 8+ arguments + }), + ) as Observable; } private cancelNavigationTransition( - t: NavigationTransition, reason: string, code: NavigationCancellationCode) { - const navCancel = - new NavigationCancel(t.id, this.urlSerializer.serialize(t.extractedUrl), reason, code); + t: NavigationTransition, + reason: string, + code: NavigationCancellationCode, + ) { + const navCancel = new NavigationCancel( + t.id, + this.urlSerializer.serialize(t.extractedUrl), + reason, + code, + ); this.events.next(navCancel); t.resolve(false); } @@ -759,8 +876,10 @@ export class NavigationTransitions { // [Object object] with the custom serializer and be "the same" when they // aren't). // (Same for isUpdatedBrowserUrl) - return this.currentTransition?.extractedUrl.toString() !== - this.currentTransition?.currentUrlTree.toString(); + return ( + this.currentTransition?.extractedUrl.toString() !== + this.currentTransition?.currentUrlTree.toString() + ); } /** @@ -772,10 +891,13 @@ export class NavigationTransitions { // The extracted URL is the part of the URL that this application cares about. `extract` may // return only part of the browser URL and that part may have not changed even if some other // portion of the URL did. - const extractedBrowserUrl = - this.urlHandlingStrategy.extract(this.urlSerializer.parse(this.location.path(true))); - return extractedBrowserUrl.toString() !== this.currentTransition?.extractedUrl.toString() && - !this.currentTransition?.extras.skipLocationChange; + const extractedBrowserUrl = this.urlHandlingStrategy.extract( + this.urlSerializer.parse(this.location.path(true)), + ); + return ( + extractedBrowserUrl.toString() !== this.currentTransition?.extractedUrl.toString() && + !this.currentTransition?.extras.skipLocationChange + ); } } diff --git a/packages/router/src/operators/activate_routes.ts b/packages/router/src/operators/activate_routes.ts index 89077582669..e2f872b2e74 100644 --- a/packages/router/src/operators/activate_routes.ts +++ b/packages/router/src/operators/activate_routes.ts @@ -19,22 +19,31 @@ import {nodeChildrenAsMap, TreeNode} from '../utils/tree'; let warnedAboutUnsupportedInputBinding = false; -export const activateRoutes = - (rootContexts: ChildrenOutletContexts, routeReuseStrategy: RouteReuseStrategy, - forwardEvent: (evt: Event) => void, - inputBindingEnabled: boolean): MonoTypeOperatorFunction => map(t => { - new ActivateRoutes( - routeReuseStrategy, t.targetRouterState!, t.currentRouterState, forwardEvent, - inputBindingEnabled) - .activate(rootContexts); - return t; - }); +export const activateRoutes = ( + rootContexts: ChildrenOutletContexts, + routeReuseStrategy: RouteReuseStrategy, + forwardEvent: (evt: Event) => void, + inputBindingEnabled: boolean, +): MonoTypeOperatorFunction => + map((t) => { + new ActivateRoutes( + routeReuseStrategy, + t.targetRouterState!, + t.currentRouterState, + forwardEvent, + inputBindingEnabled, + ).activate(rootContexts); + return t; + }); export class ActivateRoutes { constructor( - private routeReuseStrategy: RouteReuseStrategy, private futureState: RouterState, - private currState: RouterState, private forwardEvent: (evt: Event) => void, - private inputBindingEnabled: boolean) {} + private routeReuseStrategy: RouteReuseStrategy, + private futureState: RouterState, + private currState: RouterState, + private forwardEvent: (evt: Event) => void, + private inputBindingEnabled: boolean, + ) {} activate(parentContexts: ChildrenOutletContexts): void { const futureRoot = this.futureState._root; @@ -47,12 +56,14 @@ export class ActivateRoutes { // De-activate the child route that are not re-used for the future state private deactivateChildRoutes( - futureNode: TreeNode, currNode: TreeNode|null, - contexts: ChildrenOutletContexts): void { + futureNode: TreeNode, + currNode: TreeNode | null, + contexts: ChildrenOutletContexts, + ): void { const children: {[outletName: string]: TreeNode} = nodeChildrenAsMap(currNode); // Recurse on the routes active in the future state to de-activate deeper children - futureNode.children.forEach(futureChild => { + futureNode.children.forEach((futureChild) => { const childOutletName = futureChild.value.outlet; this.deactivateRoutes(futureChild, children[childOutletName], contexts); delete children[childOutletName]; @@ -65,8 +76,10 @@ export class ActivateRoutes { } private deactivateRoutes( - futureNode: TreeNode, currNode: TreeNode, - parentContext: ChildrenOutletContexts): void { + futureNode: TreeNode, + currNode: TreeNode, + parentContext: ChildrenOutletContexts, + ): void { const future = futureNode.value; const curr = currNode ? currNode.value : null; @@ -91,7 +104,9 @@ export class ActivateRoutes { } private deactivateRouteAndItsChildren( - route: TreeNode, parentContexts: ChildrenOutletContexts): void { + route: TreeNode, + parentContexts: ChildrenOutletContexts, + ): void { // If there is no component, the Route is never attached to an outlet (because there is no // component to attach). if (route.value.component && this.routeReuseStrategy.shouldDetach(route.value.snapshot)) { @@ -102,7 +117,9 @@ export class ActivateRoutes { } private detachAndStoreRouteSubtree( - route: TreeNode, parentContexts: ChildrenOutletContexts): void { + route: TreeNode, + parentContexts: ChildrenOutletContexts, + ): void { const context = parentContexts.getContext(route.value.outlet); const contexts = context && route.value.component ? context.children : parentContexts; const children: {[outletName: string]: TreeNode} = nodeChildrenAsMap(route); @@ -119,7 +136,9 @@ export class ActivateRoutes { } private deactivateRouteAndOutlet( - route: TreeNode, parentContexts: ChildrenOutletContexts): void { + route: TreeNode, + parentContexts: ChildrenOutletContexts, + ): void { const context = parentContexts.getContext(route.value.outlet); // The context could be `null` if we are on a componentless route but there may still be // children that need deactivating. @@ -146,10 +165,12 @@ export class ActivateRoutes { } private activateChildRoutes( - futureNode: TreeNode, currNode: TreeNode|null, - contexts: ChildrenOutletContexts): void { + futureNode: TreeNode, + currNode: TreeNode | null, + contexts: ChildrenOutletContexts, + ): void { const children: {[outlet: string]: TreeNode} = nodeChildrenAsMap(currNode); - futureNode.children.forEach(c => { + futureNode.children.forEach((c) => { this.activateRoutes(c, children[c.value.outlet], contexts); this.forwardEvent(new ActivationEnd(c.value.snapshot)); }); @@ -159,8 +180,10 @@ export class ActivateRoutes { } private activateRoutes( - futureNode: TreeNode, currNode: TreeNode, - parentContexts: ChildrenOutletContexts): void { + futureNode: TreeNode, + currNode: TreeNode, + parentContexts: ChildrenOutletContexts, + ): void { const future = futureNode.value; const curr = currNode ? currNode.value : null; @@ -182,8 +205,9 @@ export class ActivateRoutes { const context = parentContexts.getOrCreateContext(future.outlet); if (this.routeReuseStrategy.shouldAttach(future.snapshot)) { - const stored = - (this.routeReuseStrategy.retrieve(future.snapshot)); + const stored = ( + this.routeReuseStrategy.retrieve(future.snapshot) + ); this.routeReuseStrategy.store(future.snapshot, null); context.children.onOutletReAttached(stored.contexts); context.attachRef = stored.componentRef; @@ -214,14 +238,19 @@ export class ActivateRoutes { this.activateChildRoutes(futureNode, null, parentContexts); } } - if ((typeof ngDevMode === 'undefined' || ngDevMode)) { + if (typeof ngDevMode === 'undefined' || ngDevMode) { const context = parentContexts.getOrCreateContext(future.outlet); const outlet = context.outlet; - if (outlet && this.inputBindingEnabled && !outlet.supportsBindingToComponentInputs && - !warnedAboutUnsupportedInputBinding) { + if ( + outlet && + this.inputBindingEnabled && + !outlet.supportsBindingToComponentInputs && + !warnedAboutUnsupportedInputBinding + ) { console.warn( - `'withComponentInputBinding' feature is enabled but ` + - `this application is using an outlet that may not support binding to component inputs.`); + `'withComponentInputBinding' feature is enabled but ` + + `this application is using an outlet that may not support binding to component inputs.`, + ); warnedAboutUnsupportedInputBinding = true; } } diff --git a/packages/router/src/operators/check_guards.ts b/packages/router/src/operators/check_guards.ts index d5107dbd21a..fe0c5b26a22 100644 --- a/packages/router/src/operators/check_guards.ts +++ b/packages/router/src/operators/check_guards.ts @@ -7,66 +7,121 @@ */ import {EnvironmentInjector, ProviderToken, runInInjectionContext} from '@angular/core'; -import {concat, defer, from, MonoTypeOperatorFunction, Observable, of, OperatorFunction, pipe} from 'rxjs'; +import { + concat, + defer, + from, + MonoTypeOperatorFunction, + Observable, + of, + OperatorFunction, + pipe, +} from 'rxjs'; import {concatMap, first, map, mergeMap, tap} from 'rxjs/operators'; import {ActivationStart, ChildActivationStart, Event} from '../events'; -import {CanActivateChildFn, CanActivateFn, CanDeactivateFn, CanLoadFn, CanMatchFn, Route} from '../models'; +import { + CanActivateChildFn, + CanActivateFn, + CanDeactivateFn, + CanLoadFn, + CanMatchFn, + Route, +} from '../models'; import {redirectingNavigationError} from '../navigation_canceling_error'; import {NavigationTransition} from '../navigation_transition'; import {ActivatedRouteSnapshot, RouterStateSnapshot} from '../router_state'; import {isUrlTree, UrlSegment, UrlSerializer, UrlTree} from '../url_tree'; import {wrapIntoObservable} from '../utils/collection'; import {getClosestRouteInjector} from '../utils/config'; -import {CanActivate, CanDeactivate, getCanActivateChild, getTokenOrFunctionIdentity} from '../utils/preactivation'; -import {isBoolean, isCanActivate, isCanActivateChild, isCanDeactivate, isCanLoad, isCanMatch} from '../utils/type_guards'; +import { + CanActivate, + CanDeactivate, + getCanActivateChild, + getTokenOrFunctionIdentity, +} from '../utils/preactivation'; +import { + isBoolean, + isCanActivate, + isCanActivateChild, + isCanDeactivate, + isCanLoad, + isCanMatch, +} from '../utils/type_guards'; import {prioritizedGuardValue} from './prioritized_guard_value'; -export function checkGuards(injector: EnvironmentInjector, forwardEvent?: (evt: Event) => void): - MonoTypeOperatorFunction { - return mergeMap(t => { - const {targetSnapshot, currentSnapshot, guards: {canActivateChecks, canDeactivateChecks}} = t; +export function checkGuards( + injector: EnvironmentInjector, + forwardEvent?: (evt: Event) => void, +): MonoTypeOperatorFunction { + return mergeMap((t) => { + const { + targetSnapshot, + currentSnapshot, + guards: {canActivateChecks, canDeactivateChecks}, + } = t; if (canDeactivateChecks.length === 0 && canActivateChecks.length === 0) { return of({...t, guardsResult: true}); } - return runCanDeactivateChecks(canDeactivateChecks, targetSnapshot!, currentSnapshot, injector) - .pipe( - mergeMap(canDeactivate => { - return canDeactivate && isBoolean(canDeactivate) ? - runCanActivateChecks(targetSnapshot!, canActivateChecks, injector, forwardEvent) : - of(canDeactivate); - }), - map(guardsResult => ({...t, guardsResult}))); + return runCanDeactivateChecks( + canDeactivateChecks, + targetSnapshot!, + currentSnapshot, + injector, + ).pipe( + mergeMap((canDeactivate) => { + return canDeactivate && isBoolean(canDeactivate) + ? runCanActivateChecks(targetSnapshot!, canActivateChecks, injector, forwardEvent) + : of(canDeactivate); + }), + map((guardsResult) => ({...t, guardsResult})), + ); }); } function runCanDeactivateChecks( - checks: CanDeactivate[], futureRSS: RouterStateSnapshot, currRSS: RouterStateSnapshot, - injector: EnvironmentInjector) { + checks: CanDeactivate[], + futureRSS: RouterStateSnapshot, + currRSS: RouterStateSnapshot, + injector: EnvironmentInjector, +) { return from(checks).pipe( - mergeMap( - check => runCanDeactivate(check.component, check.route, currRSS, futureRSS, injector)), - first(result => { + mergeMap((check) => + runCanDeactivate(check.component, check.route, currRSS, futureRSS, injector), + ), + first( + (result) => { return result !== true; - }, true as boolean | UrlTree)); + }, + true as boolean | UrlTree, + ), + ); } function runCanActivateChecks( - futureSnapshot: RouterStateSnapshot, checks: CanActivate[], injector: EnvironmentInjector, - forwardEvent?: (evt: Event) => void) { + futureSnapshot: RouterStateSnapshot, + checks: CanActivate[], + injector: EnvironmentInjector, + forwardEvent?: (evt: Event) => void, +) { return from(checks).pipe( - concatMap((check: CanActivate) => { - return concat( - fireChildActivationStart(check.route.parent, forwardEvent), - fireActivationStart(check.route, forwardEvent), - runCanActivateChild(futureSnapshot, check.path, injector), - runCanActivate(futureSnapshot, check.route, injector)); - }), - first(result => { + concatMap((check: CanActivate) => { + return concat( + fireChildActivationStart(check.route.parent, forwardEvent), + fireActivationStart(check.route, forwardEvent), + runCanActivateChild(futureSnapshot, check.path, injector), + runCanActivate(futureSnapshot, check.route, injector), + ); + }), + first( + (result) => { return result !== true; - }, true as boolean | UrlTree)); + }, + true as boolean | UrlTree, + ), + ); } /** @@ -78,8 +133,9 @@ function runCanActivateChecks( * `true` so checks continue to run. */ function fireActivationStart( - snapshot: ActivatedRouteSnapshot|null, - forwardEvent?: (evt: Event) => void): Observable { + snapshot: ActivatedRouteSnapshot | null, + forwardEvent?: (evt: Event) => void, +): Observable { if (snapshot !== null && forwardEvent) { forwardEvent(new ActivationStart(snapshot)); } @@ -95,8 +151,9 @@ function fireActivationStart( * `true` so checks continue to run. */ function fireChildActivationStart( - snapshot: ActivatedRouteSnapshot|null, - forwardEvent?: (evt: Event) => void): Observable { + snapshot: ActivatedRouteSnapshot | null, + forwardEvent?: (evt: Event) => void, +): Observable { if (snapshot !== null && forwardEvent) { forwardEvent(new ChildActivationStart(snapshot)); } @@ -104,49 +161,60 @@ function fireChildActivationStart( } function runCanActivate( - futureRSS: RouterStateSnapshot, futureARS: ActivatedRouteSnapshot, - injector: EnvironmentInjector): Observable { + futureRSS: RouterStateSnapshot, + futureARS: ActivatedRouteSnapshot, + injector: EnvironmentInjector, +): Observable { const canActivate = futureARS.routeConfig ? futureARS.routeConfig.canActivate : null; if (!canActivate || canActivate.length === 0) return of(true); - const canActivateObservables = - canActivate.map((canActivate: CanActivateFn|ProviderToken) => { - return defer(() => { - const closestInjector = getClosestRouteInjector(futureARS) ?? injector; - const guard = getTokenOrFunctionIdentity(canActivate, closestInjector); - const guardVal = isCanActivate(guard) ? - guard.canActivate(futureARS, futureRSS) : - runInInjectionContext( - closestInjector, () => (guard as CanActivateFn)(futureARS, futureRSS)); - return wrapIntoObservable(guardVal).pipe(first()); - }); + const canActivateObservables = canActivate.map( + (canActivate: CanActivateFn | ProviderToken) => { + return defer(() => { + const closestInjector = getClosestRouteInjector(futureARS) ?? injector; + const guard = getTokenOrFunctionIdentity(canActivate, closestInjector); + const guardVal = isCanActivate(guard) + ? guard.canActivate(futureARS, futureRSS) + : runInInjectionContext(closestInjector, () => + (guard as CanActivateFn)(futureARS, futureRSS), + ); + return wrapIntoObservable(guardVal).pipe(first()); }); + }, + ); return of(canActivateObservables).pipe(prioritizedGuardValue()); } function runCanActivateChild( - futureRSS: RouterStateSnapshot, path: ActivatedRouteSnapshot[], - injector: EnvironmentInjector): Observable { + futureRSS: RouterStateSnapshot, + path: ActivatedRouteSnapshot[], + injector: EnvironmentInjector, +): Observable { const futureARS = path[path.length - 1]; - const canActivateChildGuards = path.slice(0, path.length - 1) - .reverse() - .map(p => getCanActivateChild(p)) - .filter(_ => _ !== null); + const canActivateChildGuards = path + .slice(0, path.length - 1) + .reverse() + .map((p) => getCanActivateChild(p)) + .filter((_) => _ !== null); const canActivateChildGuardsMapped = canActivateChildGuards.map((d: any) => { return defer(() => { - const guardsMapped = - d.guards.map((canActivateChild: CanActivateChildFn|ProviderToken) => { - const closestInjector = getClosestRouteInjector(d.node) ?? injector; - const guard = getTokenOrFunctionIdentity<{canActivateChild: CanActivateChildFn}>( - canActivateChild, closestInjector); - const guardVal = isCanActivateChild(guard) ? - guard.canActivateChild(futureARS, futureRSS) : - runInInjectionContext( - closestInjector, () => (guard as CanActivateChildFn)(futureARS, futureRSS)); - return wrapIntoObservable(guardVal).pipe(first()); - }); + const guardsMapped = d.guards.map( + (canActivateChild: CanActivateChildFn | ProviderToken) => { + const closestInjector = getClosestRouteInjector(d.node) ?? injector; + const guard = getTokenOrFunctionIdentity<{canActivateChild: CanActivateChildFn}>( + canActivateChild, + closestInjector, + ); + const guardVal = isCanActivateChild(guard) + ? guard.canActivateChild(futureARS, futureRSS) + : runInInjectionContext(closestInjector, () => + (guard as CanActivateChildFn)(futureARS, futureRSS), + ); + return wrapIntoObservable(guardVal).pipe(first()); + }, + ); return of(guardsMapped).pipe(prioritizedGuardValue()); }); }); @@ -154,26 +222,33 @@ function runCanActivateChild( } function runCanDeactivate( - component: Object|null, currARS: ActivatedRouteSnapshot, currRSS: RouterStateSnapshot, - futureRSS: RouterStateSnapshot, injector: EnvironmentInjector): Observable { + component: Object | null, + currARS: ActivatedRouteSnapshot, + currRSS: RouterStateSnapshot, + futureRSS: RouterStateSnapshot, + injector: EnvironmentInjector, +): Observable { const canDeactivate = currARS && currARS.routeConfig ? currARS.routeConfig.canDeactivate : null; if (!canDeactivate || canDeactivate.length === 0) return of(true); const canDeactivateObservables = canDeactivate.map((c: any) => { const closestInjector = getClosestRouteInjector(currARS) ?? injector; const guard = getTokenOrFunctionIdentity(c, closestInjector); - const guardVal = isCanDeactivate(guard) ? - guard.canDeactivate(component, currARS, currRSS, futureRSS) : - runInInjectionContext( - closestInjector, - () => (guard as CanDeactivateFn)(component, currARS, currRSS, futureRSS)); + const guardVal = isCanDeactivate(guard) + ? guard.canDeactivate(component, currARS, currRSS, futureRSS) + : runInInjectionContext(closestInjector, () => + (guard as CanDeactivateFn)(component, currARS, currRSS, futureRSS), + ); return wrapIntoObservable(guardVal).pipe(first()); }); return of(canDeactivateObservables).pipe(prioritizedGuardValue()); } export function runCanLoadGuards( - injector: EnvironmentInjector, route: Route, segments: UrlSegment[], - urlSerializer: UrlSerializer): Observable { + injector: EnvironmentInjector, + route: Route, + segments: UrlSegment[], + urlSerializer: UrlSerializer, +): Observable { const canLoad = route.canLoad; if (canLoad === undefined || canLoad.length === 0) { return of(true); @@ -181,48 +256,44 @@ export function runCanLoadGuards( const canLoadObservables = canLoad.map((injectionToken: any) => { const guard = getTokenOrFunctionIdentity(injectionToken, injector); - const guardVal = isCanLoad(guard) ? - guard.canLoad(route, segments) : - runInInjectionContext(injector, () => (guard as CanLoadFn)(route, segments)); + const guardVal = isCanLoad(guard) + ? guard.canLoad(route, segments) + : runInInjectionContext(injector, () => (guard as CanLoadFn)(route, segments)); return wrapIntoObservable(guardVal); }); - return of(canLoadObservables) - .pipe( - prioritizedGuardValue(), - redirectIfUrlTree(urlSerializer), - ); + return of(canLoadObservables).pipe(prioritizedGuardValue(), redirectIfUrlTree(urlSerializer)); } -function redirectIfUrlTree(urlSerializer: UrlSerializer): - OperatorFunction { +function redirectIfUrlTree( + urlSerializer: UrlSerializer, +): OperatorFunction { return pipe( - tap((result: UrlTree|boolean) => { - if (!isUrlTree(result)) return; + tap((result: UrlTree | boolean) => { + if (!isUrlTree(result)) return; - throw redirectingNavigationError(urlSerializer, result); - }), - map(result => result === true), + throw redirectingNavigationError(urlSerializer, result); + }), + map((result) => result === true), ); } export function runCanMatchGuards( - injector: EnvironmentInjector, route: Route, segments: UrlSegment[], - urlSerializer: UrlSerializer): Observable { + injector: EnvironmentInjector, + route: Route, + segments: UrlSegment[], + urlSerializer: UrlSerializer, +): Observable { const canMatch = route.canMatch; if (!canMatch || canMatch.length === 0) return of(true); - const canMatchObservables = canMatch.map(injectionToken => { + const canMatchObservables = canMatch.map((injectionToken) => { const guard = getTokenOrFunctionIdentity(injectionToken, injector); - const guardVal = isCanMatch(guard) ? - guard.canMatch(route, segments) : - runInInjectionContext(injector, () => (guard as CanMatchFn)(route, segments)); + const guardVal = isCanMatch(guard) + ? guard.canMatch(route, segments) + : runInInjectionContext(injector, () => (guard as CanMatchFn)(route, segments)); return wrapIntoObservable(guardVal); }); - return of(canMatchObservables) - .pipe( - prioritizedGuardValue(), - redirectIfUrlTree(urlSerializer), - ); + return of(canMatchObservables).pipe(prioritizedGuardValue(), redirectIfUrlTree(urlSerializer)); } diff --git a/packages/router/src/operators/prioritized_guard_value.ts b/packages/router/src/operators/prioritized_guard_value.ts index c7f0a70f254..2a483f2aa7b 100644 --- a/packages/router/src/operators/prioritized_guard_value.ts +++ b/packages/router/src/operators/prioritized_guard_value.ts @@ -14,31 +14,34 @@ import {UrlTree} from '../url_tree'; const INITIAL_VALUE = /* @__PURE__ */ Symbol('INITIAL_VALUE'); declare type INTERIM_VALUES = typeof INITIAL_VALUE | boolean | UrlTree; -export function prioritizedGuardValue(): - OperatorFunction[], boolean|UrlTree> { - return switchMap(obs => { - return combineLatest(obs.map(o => o.pipe(take(1), startWith(INITIAL_VALUE as INTERIM_VALUES)))) - .pipe( - map((results: INTERIM_VALUES[]) => { - for (const result of results) { - if (result === true) { - // If result is true, check the next one - continue; - } else if (result === INITIAL_VALUE) { - // If guard has not finished, we need to stop processing. - return INITIAL_VALUE; - } else if (result === false || result instanceof UrlTree) { - // Result finished and was not true. Return the result. - // Note that we only allow false/UrlTree. Other values are considered invalid and - // ignored. - return result; - } - } - // Everything resolved to true. Return true. - return true; - }), - filter((item): item is boolean|UrlTree => item !== INITIAL_VALUE), - take(1), - ); +export function prioritizedGuardValue(): OperatorFunction< + Observable[], + boolean | UrlTree +> { + return switchMap((obs) => { + return combineLatest( + obs.map((o) => o.pipe(take(1), startWith(INITIAL_VALUE as INTERIM_VALUES))), + ).pipe( + map((results: INTERIM_VALUES[]) => { + for (const result of results) { + if (result === true) { + // If result is true, check the next one + continue; + } else if (result === INITIAL_VALUE) { + // If guard has not finished, we need to stop processing. + return INITIAL_VALUE; + } else if (result === false || result instanceof UrlTree) { + // Result finished and was not true. Return the result. + // Note that we only allow false/UrlTree. Other values are considered invalid and + // ignored. + return result; + } + } + // Everything resolved to true. Return true. + return true; + }), + filter((item): item is boolean | UrlTree => item !== INITIAL_VALUE), + take(1), + ); }); } diff --git a/packages/router/src/operators/recognize.ts b/packages/router/src/operators/recognize.ts index c2c39b43a95..d3b8b9b67d6 100644 --- a/packages/router/src/operators/recognize.ts +++ b/packages/router/src/operators/recognize.ts @@ -17,15 +17,26 @@ import {RouterConfigLoader} from '../router_config_loader'; import {UrlSerializer} from '../url_tree'; export function recognize( - injector: EnvironmentInjector, configLoader: RouterConfigLoader, - rootComponentType: Type|null, config: Route[], serializer: UrlSerializer, - paramsInheritanceStrategy: 'emptyOnly'| - 'always'): MonoTypeOperatorFunction { - return mergeMap( - t => recognizeFn( - injector, configLoader, rootComponentType, config, t.extractedUrl, serializer, - paramsInheritanceStrategy) - .pipe(map(({state: targetSnapshot, tree: urlAfterRedirects}) => { - return {...t, targetSnapshot, urlAfterRedirects}; - }))); + injector: EnvironmentInjector, + configLoader: RouterConfigLoader, + rootComponentType: Type | null, + config: Route[], + serializer: UrlSerializer, + paramsInheritanceStrategy: 'emptyOnly' | 'always', +): MonoTypeOperatorFunction { + return mergeMap((t) => + recognizeFn( + injector, + configLoader, + rootComponentType, + config, + t.extractedUrl, + serializer, + paramsInheritanceStrategy, + ).pipe( + map(({state: targetSnapshot, tree: urlAfterRedirects}) => { + return {...t, targetSnapshot, urlAfterRedirects}; + }), + ), + ); } diff --git a/packages/router/src/operators/resolve_data.ts b/packages/router/src/operators/resolve_data.ts index 32279738d74..77d5e153f77 100644 --- a/packages/router/src/operators/resolve_data.ts +++ b/packages/router/src/operators/resolve_data.ts @@ -12,7 +12,12 @@ import {catchError, concatMap, first, map, mapTo, mergeMap, takeLast, tap} from import {ResolveData} from '../models'; import {NavigationTransition} from '../navigation_transition'; -import {ActivatedRouteSnapshot, getInherited, hasStaticTitle, RouterStateSnapshot} from '../router_state'; +import { + ActivatedRouteSnapshot, + getInherited, + hasStaticTitle, + RouterStateSnapshot, +} from '../router_state'; import {RouteTitleKey} from '../shared'; import {getDataKeys, wrapIntoObservable} from '../utils/collection'; import {getClosestRouteInjector} from '../utils/config'; @@ -20,10 +25,14 @@ import {getTokenOrFunctionIdentity} from '../utils/preactivation'; import {isEmptyError} from '../utils/type_guards'; export function resolveData( - paramsInheritanceStrategy: 'emptyOnly'|'always', - injector: EnvironmentInjector): MonoTypeOperatorFunction { - return mergeMap(t => { - const {targetSnapshot, guards: {canActivateChecks}} = t; + paramsInheritanceStrategy: 'emptyOnly' | 'always', + injector: EnvironmentInjector, +): MonoTypeOperatorFunction { + return mergeMap((t) => { + const { + targetSnapshot, + guards: {canActivateChecks}, + } = t; if (!canActivateChecks.length) { return of(t); @@ -31,7 +40,7 @@ export function resolveData( // Iterating a Set in javascript happens in insertion order so it is safe to use a `Set` to // preserve the correct order that the resolvers should run in. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#description - const routesWithResolversToRun = new Set(canActivateChecks.map(check => check.route)); + const routesWithResolversToRun = new Set(canActivateChecks.map((check) => check.route)); const routesNeedingDataUpdates = new Set(); for (const route of routesWithResolversToRun) { if (routesNeedingDataUpdates.has(route)) { @@ -43,20 +52,19 @@ export function resolveData( } } let routesProcessed = 0; - return from(routesNeedingDataUpdates) - .pipe( - concatMap(route => { - if (routesWithResolversToRun.has(route)) { - return runResolve(route, targetSnapshot!, paramsInheritanceStrategy, injector); - } else { - route.data = getInherited(route, route.parent, paramsInheritanceStrategy).resolve; - return of(void 0); - } - }), - tap(() => routesProcessed++), - takeLast(1), - mergeMap(_ => routesProcessed === routesNeedingDataUpdates.size ? of(t) : EMPTY), - ); + return from(routesNeedingDataUpdates).pipe( + concatMap((route) => { + if (routesWithResolversToRun.has(route)) { + return runResolve(route, targetSnapshot!, paramsInheritanceStrategy, injector); + } else { + route.data = getInherited(route, route.parent, paramsInheritanceStrategy).resolve; + return of(void 0); + } + }), + tap(() => routesProcessed++), + takeLast(1), + mergeMap((_) => (routesProcessed === routesNeedingDataUpdates.size ? of(t) : EMPTY)), + ); }); } @@ -64,52 +72,66 @@ export function resolveData( * Returns the `ActivatedRouteSnapshot` tree as an array, using DFS to traverse the route tree. */ function flattenRouteTree(route: ActivatedRouteSnapshot): ActivatedRouteSnapshot[] { - const descendants = route.children.map(child => flattenRouteTree(child)).flat(); + const descendants = route.children.map((child) => flattenRouteTree(child)).flat(); return [route, ...descendants]; } function runResolve( - futureARS: ActivatedRouteSnapshot, futureRSS: RouterStateSnapshot, - paramsInheritanceStrategy: 'emptyOnly'|'always', injector: EnvironmentInjector) { + futureARS: ActivatedRouteSnapshot, + futureRSS: RouterStateSnapshot, + paramsInheritanceStrategy: 'emptyOnly' | 'always', + injector: EnvironmentInjector, +) { const config = futureARS.routeConfig; const resolve = futureARS._resolve; if (config?.title !== undefined && !hasStaticTitle(config)) { resolve[RouteTitleKey] = config.title; } - return resolveNode(resolve, futureARS, futureRSS, injector).pipe(map((resolvedData: any) => { - futureARS._resolvedData = resolvedData; - futureARS.data = getInherited(futureARS, futureARS.parent, paramsInheritanceStrategy).resolve; - return null; - })); + return resolveNode(resolve, futureARS, futureRSS, injector).pipe( + map((resolvedData: any) => { + futureARS._resolvedData = resolvedData; + futureARS.data = getInherited(futureARS, futureARS.parent, paramsInheritanceStrategy).resolve; + return null; + }), + ); } function resolveNode( - resolve: ResolveData, futureARS: ActivatedRouteSnapshot, futureRSS: RouterStateSnapshot, - injector: EnvironmentInjector): Observable { + resolve: ResolveData, + futureARS: ActivatedRouteSnapshot, + futureRSS: RouterStateSnapshot, + injector: EnvironmentInjector, +): Observable { const keys = getDataKeys(resolve); if (keys.length === 0) { return of({}); } - const data: {[k: string|symbol]: any} = {}; + const data: {[k: string | symbol]: any} = {}; return from(keys).pipe( - mergeMap( - key => getResolver(resolve[key], futureARS, futureRSS, injector) - .pipe(first(), tap((value: any) => { - data[key] = value; - }))), - takeLast(1), - mapTo(data), - catchError((e: unknown) => isEmptyError(e as Error) ? EMPTY : throwError(e)), + mergeMap((key) => + getResolver(resolve[key], futureARS, futureRSS, injector).pipe( + first(), + tap((value: any) => { + data[key] = value; + }), + ), + ), + takeLast(1), + mapTo(data), + catchError((e: unknown) => (isEmptyError(e as Error) ? EMPTY : throwError(e))), ); } function getResolver( - injectionToken: ProviderToken|Function, futureARS: ActivatedRouteSnapshot, - futureRSS: RouterStateSnapshot, injector: EnvironmentInjector): Observable { + injectionToken: ProviderToken | Function, + futureARS: ActivatedRouteSnapshot, + futureRSS: RouterStateSnapshot, + injector: EnvironmentInjector, +): Observable { const closestInjector = getClosestRouteInjector(futureARS) ?? injector; const resolver = getTokenOrFunctionIdentity(injectionToken, closestInjector); - const resolverValue = resolver.resolve ? - resolver.resolve(futureARS, futureRSS) : - runInInjectionContext(closestInjector, () => resolver(futureARS, futureRSS)); + const resolverValue = resolver.resolve + ? resolver.resolve(futureARS, futureRSS) + : runInInjectionContext(closestInjector, () => resolver(futureARS, futureRSS)); return wrapIntoObservable(resolverValue); } diff --git a/packages/router/src/operators/switch_tap.ts b/packages/router/src/operators/switch_tap.ts index cd53a02e553..62e4e483d70 100644 --- a/packages/router/src/operators/switch_tap.ts +++ b/packages/router/src/operators/switch_tap.ts @@ -15,9 +15,10 @@ import {map, switchMap} from 'rxjs/operators'; * the `tap` operator, but if the side effectful `next` function returns an ObservableInput, * it will wait before continuing with the original value. */ -export function switchTap(next: (x: T) => void|ObservableInput): - MonoTypeOperatorFunction { - return switchMap(v => { +export function switchTap( + next: (x: T) => void | ObservableInput, +): MonoTypeOperatorFunction { + return switchMap((v) => { const nextResult = next(v); if (nextResult) { return from(nextResult).pipe(map(() => v)); diff --git a/packages/router/src/page_title_strategy.ts b/packages/router/src/page_title_strategy.ts index cb5d5410b26..b401e568464 100644 --- a/packages/router/src/page_title_strategy.ts +++ b/packages/router/src/page_title_strategy.ts @@ -43,12 +43,12 @@ export abstract class TitleStrategy { /** * @returns The `title` of the deepest primary route. */ - buildTitle(snapshot: RouterStateSnapshot): string|undefined { - let pageTitle: string|undefined; - let route: ActivatedRouteSnapshot|undefined = snapshot.root; + buildTitle(snapshot: RouterStateSnapshot): string | undefined { + let pageTitle: string | undefined; + let route: ActivatedRouteSnapshot | undefined = snapshot.root; while (route !== undefined) { pageTitle = this.getResolvedTitleForRoute(route) ?? pageTitle; - route = route.children.find(child => child.outlet === PRIMARY_OUTLET); + route = route.children.find((child) => child.outlet === PRIMARY_OUTLET); } return pageTitle; } diff --git a/packages/router/src/private_export.ts b/packages/router/src/private_export.ts index 5307a51f2e6..8318ed0c992 100644 --- a/packages/router/src/private_export.ts +++ b/packages/router/src/private_export.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ - export {ɵEmptyOutletComponent} from './components/empty_outlet'; export {RestoredState as ɵRestoredState} from './navigation_transition'; export {loadChildren as ɵloadChildren} from './router_config_loader'; diff --git a/packages/router/src/provide_router.ts b/packages/router/src/provide_router.ts index 36b5df2243b..036eb76b889 100644 --- a/packages/router/src/provide_router.ts +++ b/packages/router/src/provide_router.ts @@ -6,8 +6,30 @@ * found in the LICENSE file at https://angular.io/license */ -import {HashLocationStrategy, LOCATION_INITIALIZED, LocationStrategy, ViewportScroller} from '@angular/common'; -import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, ComponentRef, ENVIRONMENT_INITIALIZER, EnvironmentInjector, EnvironmentProviders, inject, InjectFlags, InjectionToken, Injector, makeEnvironmentProviders, NgZone, Provider, runInInjectionContext, Type} from '@angular/core'; +import { + HashLocationStrategy, + LOCATION_INITIALIZED, + LocationStrategy, + ViewportScroller, +} from '@angular/common'; +import { + APP_BOOTSTRAP_LISTENER, + APP_INITIALIZER, + ApplicationRef, + ComponentRef, + ENVIRONMENT_INITIALIZER, + EnvironmentInjector, + EnvironmentProviders, + inject, + InjectFlags, + InjectionToken, + Injector, + makeEnvironmentProviders, + NgZone, + Provider, + runInInjectionContext, + Type, +} from '@angular/core'; import {of, Subject} from 'rxjs'; import {INPUT_BINDER, RoutedComponentInputBinder} from './directives/router_outlet'; @@ -22,8 +44,12 @@ import {ROUTER_SCROLLER, RouterScroller} from './router_scroller'; import {ActivatedRoute} from './router_state'; import {UrlSerializer} from './url_tree'; import {afterNextNavigation} from './utils/navigations'; -import {CREATE_VIEW_TRANSITION, createViewTransition, VIEW_TRANSITION_OPTIONS, ViewTransitionsFeatureOptions} from './utils/view_transition'; - +import { + CREATE_VIEW_TRANSITION, + createViewTransition, + VIEW_TRANSITION_OPTIONS, + ViewTransitionsFeatureOptions, +} from './utils/view_transition'; /** * Sets up providers necessary to enable `Router` functionality for the application. @@ -64,12 +90,12 @@ import {CREATE_VIEW_TRANSITION, createViewTransition, VIEW_TRANSITION_OPTIONS, V export function provideRouter(routes: Routes, ...features: RouterFeatures[]): EnvironmentProviders { return makeEnvironmentProviders([ {provide: ROUTES, multi: true, useValue: routes}, - (typeof ngDevMode === 'undefined' || ngDevMode) ? - {provide: ROUTER_IS_PROVIDED, useValue: true} : - [], + typeof ngDevMode === 'undefined' || ngDevMode + ? {provide: ROUTER_IS_PROVIDED, useValue: true} + : [], {provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]}, {provide: APP_BOOTSTRAP_LISTENER, multi: true, useFactory: getBootstrapListener}, - features.map(feature => feature.ɵproviders), + features.map((feature) => feature.ɵproviders), ]); } @@ -91,17 +117,20 @@ export interface RouterFeature { * Helper function to create an object that represents a Router feature. */ function routerFeature( - kind: FeatureKind, providers: Provider[]): RouterFeature { + kind: FeatureKind, + providers: Provider[], +): RouterFeature { return {ɵkind: kind, ɵproviders: providers}; } - /** * An Injection token used to indicate whether `provideRouter` or `RouterModule.forRoot` was ever * called. */ -export const ROUTER_IS_PROVIDED = - new InjectionToken('', {providedIn: 'root', factory: () => false}); +export const ROUTER_IS_PROVIDED = new InjectionToken('', { + providedIn: 'root', + factory: () => false, +}); const routerIsProvidedDevModeCheck = { provide: ENVIRONMENT_INITIALIZER, @@ -110,11 +139,12 @@ const routerIsProvidedDevModeCheck = { return () => { if (!inject(ROUTER_IS_PROVIDED)) { console.warn( - '`provideRoutes` was called without `provideRouter` or `RouterModule.forRoot`. ' + - 'This is likely a mistake.'); + '`provideRoutes` was called without `provideRouter` or `RouterModule.forRoot`. ' + + 'This is likely a mistake.', + ); } }; - } + }, }; /** @@ -137,7 +167,7 @@ const routerIsProvidedDevModeCheck = { export function provideRoutes(routes: Routes): Provider[] { return [ {provide: ROUTES, multi: true, useValue: routes}, - (typeof ngDevMode === 'undefined' || ngDevMode) ? routerIsProvidedDevModeCheck : [], + typeof ngDevMode === 'undefined' || ngDevMode ? routerIsProvidedDevModeCheck : [], ]; } @@ -176,18 +206,21 @@ export type InMemoryScrollingFeature = RouterFeature { - const viewportScroller = inject(ViewportScroller); - const zone = inject(NgZone); - const transitions = inject(NavigationTransitions); - const urlSerializer = inject(UrlSerializer); - return new RouterScroller(urlSerializer, transitions, viewportScroller, zone, options); +export function withInMemoryScrolling( + options: InMemoryScrollingOptions = {}, +): InMemoryScrollingFeature { + const providers = [ + { + provide: ROUTER_SCROLLER, + useFactory: () => { + const viewportScroller = inject(ViewportScroller); + const zone = inject(NgZone); + const transitions = inject(NavigationTransitions); + const urlSerializer = inject(UrlSerializer); + return new RouterScroller(urlSerializer, transitions, viewportScroller, zone, options); + }, }, - }]; + ]; return routerFeature(RouterFeatureKind.InMemoryScrollingFeature, providers); } @@ -224,11 +257,13 @@ export function getBootstrapListener() { * to the activation phase. */ const BOOTSTRAP_DONE = new InjectionToken>( - (typeof ngDevMode === 'undefined' || ngDevMode) ? 'bootstrap done indicator' : '', { - factory: () => { - return new Subject(); - } - }); + typeof ngDevMode === 'undefined' || ngDevMode ? 'bootstrap done indicator' : '', + { + factory: () => { + return new Subject(); + }, + }, +); /** * This and the INITIAL_NAVIGATION token are used internally only. The public API side of this is @@ -254,8 +289,9 @@ const enum InitialNavigation { } const INITIAL_NAVIGATION = new InjectionToken( - (typeof ngDevMode === 'undefined' || ngDevMode) ? 'initial navigation' : '', - {providedIn: 'root', factory: () => InitialNavigation.EnabledNonBlocking}); + typeof ngDevMode === 'undefined' || ngDevMode ? 'initial navigation' : '', + {providedIn: 'root', factory: () => InitialNavigation.EnabledNonBlocking}, +); /** * A type alias for providers returned by `withEnabledBlockingInitialNavigation` for use with @@ -267,7 +303,7 @@ const INITIAL_NAVIGATION = new InjectionToken( * @publicApi */ export type EnabledBlockingInitialNavigationFeature = - RouterFeature; + RouterFeature; /** * A type alias for providers returned by `withEnabledBlockingInitialNavigation` or @@ -280,7 +316,8 @@ export type EnabledBlockingInitialNavigationFeature = * @publicApi */ export type InitialNavigationFeature = - EnabledBlockingInitialNavigationFeature|DisabledInitialNavigationFeature; + | EnabledBlockingInitialNavigationFeature + | DisabledInitialNavigationFeature; /** * Configures initial navigation to start before the root component is created. @@ -315,12 +352,14 @@ export function withEnabledBlockingInitialNavigation(): EnabledBlockingInitialNa multi: true, deps: [Injector], useFactory: (injector: Injector) => { - const locationInitialized: Promise = - injector.get(LOCATION_INITIALIZED, Promise.resolve()); + const locationInitialized: Promise = injector.get( + LOCATION_INITIALIZED, + Promise.resolve(), + ); return () => { return locationInitialized.then(() => { - return new Promise(resolve => { + return new Promise((resolve) => { const router = injector.get(Router); const bootstrapDone = injector.get(BOOTSTRAP_DONE); afterNextNavigation(router, () => { @@ -340,7 +379,7 @@ export function withEnabledBlockingInitialNavigation(): EnabledBlockingInitialNa }); }); }; - } + }, }, ]; return routerFeature(RouterFeatureKind.EnabledBlockingInitialNavigationFeature, providers); @@ -356,7 +395,7 @@ export function withEnabledBlockingInitialNavigation(): EnabledBlockingInitialNa * @publicApi */ export type DisabledInitialNavigationFeature = - RouterFeature; + RouterFeature; /** * Disables initial navigation. @@ -394,9 +433,9 @@ export function withDisabledInitialNavigation(): DisabledInitialNavigationFeatur return () => { router.setUpLocationChangeListener(); }; - } + }, }, - {provide: INITIAL_NAVIGATION, useValue: InitialNavigation.Disabled} + {provide: INITIAL_NAVIGATION, useValue: InitialNavigation.Disabled}, ]; return routerFeature(RouterFeatureKind.DisabledInitialNavigationFeature, providers); } @@ -438,21 +477,24 @@ export type DebugTracingFeature = RouterFeature { - const router = inject(Router); - return () => router.events.subscribe((e: Event) => { - // tslint:disable:no-console - console.group?.(`Router Event: ${(e.constructor).name}`); - console.log(stringifyEvent(e)); - console.log(e); - console.groupEnd?.(); - // tslint:enable:no-console - }); - } - }]; + providers = [ + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useFactory: () => { + const router = inject(Router); + return () => + router.events.subscribe((e: Event) => { + // tslint:disable:no-console + console.group?.(`Router Event: ${(e.constructor).name}`); + console.log(stringifyEvent(e)); + console.log(e); + console.groupEnd?.(); + // tslint:enable:no-console + }); + }, + }, + ]; } else { providers = []; } @@ -460,7 +502,8 @@ export function withDebugTracing(): DebugTracingFeature { } const ROUTER_PRELOADER = new InjectionToken( - (typeof ngDevMode === 'undefined' || ngDevMode) ? 'router preloader' : ''); + typeof ngDevMode === 'undefined' || ngDevMode ? 'router preloader' : '', +); /** * A type alias that represents a feature which enables preloading in Router. @@ -516,7 +559,7 @@ export function withPreloading(preloadingStrategy: Type): Pr * @publicApi */ export type RouterConfigurationFeature = - RouterFeature; + RouterFeature; /** * Allows to provide extra parameters to configure Router. @@ -546,9 +589,7 @@ export type RouterConfigurationFeature = * @publicApi */ export function withRouterConfig(options: RouterConfigOptions): RouterConfigurationFeature { - const providers = [ - {provide: ROUTER_CONFIGURATION, useValue: options}, - ]; + const providers = [{provide: ROUTER_CONFIGURATION, useValue: options}]; return routerFeature(RouterFeatureKind.RouterConfigurationFeature, providers); } @@ -587,9 +628,7 @@ export type RouterHashLocationFeature = RouterFeature; + RouterFeature; /** * Subscribes to the Router's navigation events and calls the given function when a @@ -634,20 +673,23 @@ export type NavigationErrorHandlerFeature = * * @publicApi */ -export function withNavigationErrorHandler(fn: (error: NavigationError) => void): - NavigationErrorHandlerFeature { - const providers = [{ - provide: ENVIRONMENT_INITIALIZER, - multi: true, - useValue: () => { - const injector = inject(EnvironmentInjector); - inject(Router).events.subscribe((e) => { - if (e instanceof NavigationError) { - runInInjectionContext(injector, () => fn(e)); - } - }); - } - }]; +export function withNavigationErrorHandler( + fn: (error: NavigationError) => void, +): NavigationErrorHandlerFeature { + const providers = [ + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useValue: () => { + const injector = inject(EnvironmentInjector); + inject(Router).events.subscribe((e) => { + if (e instanceof NavigationError) { + runInInjectionContext(injector, () => fn(e)); + } + }); + }, + }, + ]; return routerFeature(RouterFeatureKind.NavigationErrorHandlerFeature, providers); } @@ -660,7 +702,7 @@ export function withNavigationErrorHandler(fn: (error: NavigationError) => void) * @publicApi */ export type ComponentInputBindingFeature = - RouterFeature; + RouterFeature; /** * A type alias for providers returned by `withViewTransitions` for use with `provideRouter`. @@ -728,13 +770,14 @@ export function withComponentInputBinding(): ComponentInputBindingFeature { * @see https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API * @experimental */ -export function withViewTransitions(options?: ViewTransitionsFeatureOptions): - ViewTransitionsFeature { +export function withViewTransitions( + options?: ViewTransitionsFeatureOptions, +): ViewTransitionsFeature { const providers = [ {provide: CREATE_VIEW_TRANSITION, useValue: createViewTransition}, { provide: VIEW_TRANSITION_OPTIONS, - useValue: {skipNextTransition: !!options?.skipInitialTransition, ...options} + useValue: {skipNextTransition: !!options?.skipInitialTransition, ...options}, }, ]; return routerFeature(RouterFeatureKind.ViewTransitionsFeature, providers); @@ -750,9 +793,16 @@ export function withViewTransitions(options?: ViewTransitionsFeatureOptions): * * @publicApi */ -export type RouterFeatures = PreloadingFeature|DebugTracingFeature|InitialNavigationFeature| - InMemoryScrollingFeature|RouterConfigurationFeature|NavigationErrorHandlerFeature| - ComponentInputBindingFeature|ViewTransitionsFeature|RouterHashLocationFeature; +export type RouterFeatures = + | PreloadingFeature + | DebugTracingFeature + | InitialNavigationFeature + | InMemoryScrollingFeature + | RouterConfigurationFeature + | NavigationErrorHandlerFeature + | ComponentInputBindingFeature + | ViewTransitionsFeature + | RouterHashLocationFeature; /** * The list of features as an enum to uniquely type each feature. diff --git a/packages/router/src/recognize.ts b/packages/router/src/recognize.ts index 88ecaf7d889..0f38bf933fc 100644 --- a/packages/router/src/recognize.ts +++ b/packages/router/src/recognize.ts @@ -8,7 +8,18 @@ import {EnvironmentInjector, Type, ɵRuntimeError as RuntimeError} from '@angular/core'; import {from, Observable, of} from 'rxjs'; -import {catchError, concatMap, defaultIfEmpty, first, last, map, mergeMap, scan, switchMap, tap} from 'rxjs/operators'; +import { + catchError, + concatMap, + defaultIfEmpty, + first, + last, + map, + mergeMap, + scan, + switchMap, + tap, +} from 'rxjs/operators'; import {AbsoluteRedirect, ApplyRedirects, canLoadFails, noMatch, NoMatch} from './apply_redirects'; import {createUrlTreeFromSnapshot} from './create_url_tree'; @@ -16,11 +27,22 @@ import {RuntimeErrorCode} from './errors'; import {Data, LoadedRouterConfig, ResolveData, Route, Routes} from './models'; import {runCanLoadGuards} from './operators/check_guards'; import {RouterConfigLoader} from './router_config_loader'; -import {ActivatedRouteSnapshot, getInherited, ParamsInheritanceStrategy, RouterStateSnapshot} from './router_state'; +import { + ActivatedRouteSnapshot, + getInherited, + ParamsInheritanceStrategy, + RouterStateSnapshot, +} from './router_state'; import {PRIMARY_OUTLET} from './shared'; import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree'; import {getOutlet, sortByMatchingOutlets} from './utils/config'; -import {isImmediateMatch, match, matchWithChecks, noLeftoversInUrl, split} from './utils/config_matching'; +import { + isImmediateMatch, + match, + matchWithChecks, + noLeftoversInUrl, + split, +} from './utils/config_matching'; import {TreeNode} from './utils/tree'; import {isEmptyError} from './utils/type_guards'; @@ -32,15 +54,23 @@ import {isEmptyError} from './utils/type_guards'; class NoLeftoversInUrl {} export function recognize( - injector: EnvironmentInjector, configLoader: RouterConfigLoader, - rootComponentType: Type|null, config: Routes, urlTree: UrlTree, - urlSerializer: UrlSerializer, - paramsInheritanceStrategy: ParamsInheritanceStrategy = - 'emptyOnly'): Observable<{state: RouterStateSnapshot, tree: UrlTree}> { + injector: EnvironmentInjector, + configLoader: RouterConfigLoader, + rootComponentType: Type | null, + config: Routes, + urlTree: UrlTree, + urlSerializer: UrlSerializer, + paramsInheritanceStrategy: ParamsInheritanceStrategy = 'emptyOnly', +): Observable<{state: RouterStateSnapshot; tree: UrlTree}> { return new Recognizer( - injector, configLoader, rootComponentType, config, urlTree, paramsInheritanceStrategy, - urlSerializer) - .recognize(); + injector, + configLoader, + rootComponentType, + config, + urlTree, + paramsInheritanceStrategy, + urlSerializer, + ).recognize(); } const MAX_ALLOWED_REDIRECTS = 31; @@ -51,80 +81,115 @@ export class Recognizer { allowRedirects = true; constructor( - private injector: EnvironmentInjector, private configLoader: RouterConfigLoader, - private rootComponentType: Type|null, private config: Routes, private urlTree: UrlTree, - private paramsInheritanceStrategy: ParamsInheritanceStrategy, - private readonly urlSerializer: UrlSerializer) {} + private injector: EnvironmentInjector, + private configLoader: RouterConfigLoader, + private rootComponentType: Type | null, + private config: Routes, + private urlTree: UrlTree, + private paramsInheritanceStrategy: ParamsInheritanceStrategy, + private readonly urlSerializer: UrlSerializer, + ) {} private noMatchError(e: NoMatch): RuntimeError { return new RuntimeError( - RuntimeErrorCode.NO_MATCH, - (typeof ngDevMode === 'undefined' || ngDevMode) ? - `Cannot match any routes. URL Segment: '${e.segmentGroup}'` : - `'${e.segmentGroup}'`); + RuntimeErrorCode.NO_MATCH, + typeof ngDevMode === 'undefined' || ngDevMode + ? `Cannot match any routes. URL Segment: '${e.segmentGroup}'` + : `'${e.segmentGroup}'`, + ); } - recognize(): Observable<{state: RouterStateSnapshot, tree: UrlTree}> { + recognize(): Observable<{state: RouterStateSnapshot; tree: UrlTree}> { const rootSegmentGroup = split(this.urlTree.root, [], [], this.config).segmentGroup; - return this.match(rootSegmentGroup).pipe(map(children => { - // Use Object.freeze to prevent readers of the Router state from modifying it outside - // of a navigation, resulting in the router being out of sync with the browser. - const root = new ActivatedRouteSnapshot( - [], Object.freeze({}), Object.freeze({...this.urlTree.queryParams}), - this.urlTree.fragment, {}, PRIMARY_OUTLET, this.rootComponentType, null, {}); + return this.match(rootSegmentGroup).pipe( + map((children) => { + // Use Object.freeze to prevent readers of the Router state from modifying it outside + // of a navigation, resulting in the router being out of sync with the browser. + const root = new ActivatedRouteSnapshot( + [], + Object.freeze({}), + Object.freeze({...this.urlTree.queryParams}), + this.urlTree.fragment, + {}, + PRIMARY_OUTLET, + this.rootComponentType, + null, + {}, + ); - const rootNode = new TreeNode(root, children); - const routeState = new RouterStateSnapshot('', rootNode); - const tree = - createUrlTreeFromSnapshot(root, [], this.urlTree.queryParams, this.urlTree.fragment); - // https://github.com/angular/angular/issues/47307 - // Creating the tree stringifies the query params - // We don't want to do this here so reassign them to the original. - tree.queryParams = this.urlTree.queryParams; - routeState.url = this.urlSerializer.serialize(tree); - this.inheritParamsAndData(routeState._root, null); - return {state: routeState, tree}; - })); + const rootNode = new TreeNode(root, children); + const routeState = new RouterStateSnapshot('', rootNode); + const tree = createUrlTreeFromSnapshot( + root, + [], + this.urlTree.queryParams, + this.urlTree.fragment, + ); + // https://github.com/angular/angular/issues/47307 + // Creating the tree stringifies the query params + // We don't want to do this here so reassign them to the original. + tree.queryParams = this.urlTree.queryParams; + routeState.url = this.urlSerializer.serialize(tree); + this.inheritParamsAndData(routeState._root, null); + return {state: routeState, tree}; + }), + ); } - private match(rootSegmentGroup: UrlSegmentGroup): Observable[]> { - const expanded$ = - this.processSegmentGroup(this.injector, this.config, rootSegmentGroup, PRIMARY_OUTLET); - return expanded$.pipe(catchError((e: any) => { - if (e instanceof AbsoluteRedirect) { - this.urlTree = e.urlTree; - return this.match(e.urlTree.root); - } - if (e instanceof NoMatch) { - throw this.noMatchError(e); - } + const expanded$ = this.processSegmentGroup( + this.injector, + this.config, + rootSegmentGroup, + PRIMARY_OUTLET, + ); + return expanded$.pipe( + catchError((e: any) => { + if (e instanceof AbsoluteRedirect) { + this.urlTree = e.urlTree; + return this.match(e.urlTree.root); + } + if (e instanceof NoMatch) { + throw this.noMatchError(e); + } - throw e; - })); + throw e; + }), + ); } inheritParamsAndData( - routeNode: TreeNode, parent: ActivatedRouteSnapshot|null): void { + routeNode: TreeNode, + parent: ActivatedRouteSnapshot | null, + ): void { const route = routeNode.value; const i = getInherited(route, parent, this.paramsInheritanceStrategy); route.params = Object.freeze(i.params); route.data = Object.freeze(i.data); - routeNode.children.forEach(n => this.inheritParamsAndData(n, route)); + routeNode.children.forEach((n) => this.inheritParamsAndData(n, route)); } processSegmentGroup( - injector: EnvironmentInjector, config: Route[], segmentGroup: UrlSegmentGroup, - outlet: string): Observable[]> { + injector: EnvironmentInjector, + config: Route[], + segmentGroup: UrlSegmentGroup, + outlet: string, + ): Observable[]> { if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) { return this.processChildren(injector, config, segmentGroup); } - return this.processSegment(injector, config, segmentGroup, segmentGroup.segments, outlet, true) - .pipe(map(child => child instanceof TreeNode ? [child] : [])); + return this.processSegment( + injector, + config, + segmentGroup, + segmentGroup.segments, + outlet, + true, + ).pipe(map((child) => (child instanceof TreeNode ? [child] : []))); } /** @@ -135,8 +200,11 @@ export class Recognizer { * @param segmentGroup - The `UrlSegmentGroup` whose children need to be matched against the * config. */ - processChildren(injector: EnvironmentInjector, config: Route[], segmentGroup: UrlSegmentGroup): - Observable[]> { + processChildren( + injector: EnvironmentInjector, + config: Route[], + segmentGroup: UrlSegmentGroup, + ): Observable[]> { // Expand outlets one at a time, starting with the primary outlet. We need to do it this way // because an absolute redirect from the primary outlet takes precedence. const childOutlets: string[] = []; @@ -147,71 +215,87 @@ export class Recognizer { childOutlets.push(child); } } - return from(childOutlets) - .pipe( - concatMap(childOutlet => { - const child = segmentGroup.children[childOutlet]; - // Sort the config so that routes with outlets that match the one being activated - // appear first, followed by routes for other outlets, which might match if they have - // an empty path. - const sortedConfig = sortByMatchingOutlets(config, childOutlet); - return this.processSegmentGroup(injector, sortedConfig, child, childOutlet); - }), - scan((children, outletChildren) => { - children.push(...outletChildren); - return children; - }), - defaultIfEmpty(null as TreeNode[] | null), - last(), - mergeMap(children => { - if (children === null) return noMatch(segmentGroup); - // Because we may have matched two outlets to the same empty path segment, we can have - // multiple activated results for the same outlet. We should merge the children of - // these results so the final return value is only one `TreeNode` per outlet. - const mergedChildren = mergeEmptyPathMatches(children); - if (typeof ngDevMode === 'undefined' || ngDevMode) { - // This should really never happen - we are only taking the first match for each - // outlet and merge the empty path matches. - checkOutletNameUniqueness(mergedChildren); - } - sortActivatedRouteSnapshots(mergedChildren); - return of(mergedChildren); - }), - ); + return from(childOutlets).pipe( + concatMap((childOutlet) => { + const child = segmentGroup.children[childOutlet]; + // Sort the config so that routes with outlets that match the one being activated + // appear first, followed by routes for other outlets, which might match if they have + // an empty path. + const sortedConfig = sortByMatchingOutlets(config, childOutlet); + return this.processSegmentGroup(injector, sortedConfig, child, childOutlet); + }), + scan((children, outletChildren) => { + children.push(...outletChildren); + return children; + }), + defaultIfEmpty(null as TreeNode[] | null), + last(), + mergeMap((children) => { + if (children === null) return noMatch(segmentGroup); + // Because we may have matched two outlets to the same empty path segment, we can have + // multiple activated results for the same outlet. We should merge the children of + // these results so the final return value is only one `TreeNode` per outlet. + const mergedChildren = mergeEmptyPathMatches(children); + if (typeof ngDevMode === 'undefined' || ngDevMode) { + // This should really never happen - we are only taking the first match for each + // outlet and merge the empty path matches. + checkOutletNameUniqueness(mergedChildren); + } + sortActivatedRouteSnapshots(mergedChildren); + return of(mergedChildren); + }), + ); } processSegment( - injector: EnvironmentInjector, routes: Route[], segmentGroup: UrlSegmentGroup, - segments: UrlSegment[], outlet: string, - allowRedirects: boolean): Observable|NoLeftoversInUrl> { + injector: EnvironmentInjector, + routes: Route[], + segmentGroup: UrlSegmentGroup, + segments: UrlSegment[], + outlet: string, + allowRedirects: boolean, + ): Observable | NoLeftoversInUrl> { return from(routes).pipe( - concatMap(r => { - return this - .processSegmentAgainstRoute( - r._injector ?? injector, routes, r, segmentGroup, segments, outlet, - allowRedirects) - .pipe(catchError((e: any) => { - if (e instanceof NoMatch) { - return of(null); - } - throw e; - })); - }), - first((x): x is TreeNode|NoLeftoversInUrl => !!x), catchError(e => { - if (isEmptyError(e)) { - if (noLeftoversInUrl(segmentGroup, segments, outlet)) { - return of(new NoLeftoversInUrl()); + concatMap((r) => { + return this.processSegmentAgainstRoute( + r._injector ?? injector, + routes, + r, + segmentGroup, + segments, + outlet, + allowRedirects, + ).pipe( + catchError((e: any) => { + if (e instanceof NoMatch) { + return of(null); } - return noMatch(segmentGroup); + throw e; + }), + ); + }), + first((x): x is TreeNode | NoLeftoversInUrl => !!x), + catchError((e) => { + if (isEmptyError(e)) { + if (noLeftoversInUrl(segmentGroup, segments, outlet)) { + return of(new NoLeftoversInUrl()); } - throw e; - })); + return noMatch(segmentGroup); + } + throw e; + }), + ); } processSegmentAgainstRoute( - injector: EnvironmentInjector, routes: Route[], route: Route, rawSegment: UrlSegmentGroup, - segments: UrlSegment[], outlet: string, - allowRedirects: boolean): Observable|NoLeftoversInUrl> { + injector: EnvironmentInjector, + routes: Route[], + route: Route, + rawSegment: UrlSegmentGroup, + segments: UrlSegment[], + outlet: string, + allowRedirects: boolean, + ): Observable | NoLeftoversInUrl> { if (!isImmediateMatch(route, rawSegment, segments, outlet)) return noMatch(rawSegment); if (route.redirectTo === undefined) { @@ -220,22 +304,31 @@ export class Recognizer { if (this.allowRedirects && allowRedirects) { return this.expandSegmentAgainstRouteUsingRedirect( - injector, rawSegment, routes, route, segments, outlet); + injector, + rawSegment, + routes, + route, + segments, + outlet, + ); } return noMatch(rawSegment); } private expandSegmentAgainstRouteUsingRedirect( - injector: EnvironmentInjector, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route, - segments: UrlSegment[], - outlet: string): Observable|NoLeftoversInUrl> { - const { - matched, - consumedSegments, - positionalParamSegments, - remainingSegments, - } = match(segmentGroup, route, segments); + injector: EnvironmentInjector, + segmentGroup: UrlSegmentGroup, + routes: Route[], + route: Route, + segments: UrlSegment[], + outlet: string, + ): Observable | NoLeftoversInUrl> { + const {matched, consumedSegments, positionalParamSegments, remainingSegments} = match( + segmentGroup, + route, + segments, + ); if (!matched) return noMatch(segmentGroup); // TODO(atscott): Move all of this under an if(ngDevMode) as a breaking change and allow stack @@ -245,28 +338,42 @@ export class Recognizer { if (this.absoluteRedirectCount > MAX_ALLOWED_REDIRECTS) { if (ngDevMode) { throw new RuntimeError( - RuntimeErrorCode.INFINITE_REDIRECT, - `Detected possible infinite redirect when redirecting from '${this.urlTree}' to '${ - route.redirectTo}'.\n` + - `This is currently a dev mode only error but will become a` + - ` call stack size exceeded error in production in a future major version.`); + RuntimeErrorCode.INFINITE_REDIRECT, + `Detected possible infinite redirect when redirecting from '${this.urlTree}' to '${route.redirectTo}'.\n` + + `This is currently a dev mode only error but will become a` + + ` call stack size exceeded error in production in a future major version.`, + ); } this.allowRedirects = false; } } const newTree = this.applyRedirects.applyRedirectCommands( - consumedSegments, route.redirectTo!, positionalParamSegments); + consumedSegments, + route.redirectTo!, + positionalParamSegments, + ); - return this.applyRedirects.lineralizeSegments(route, newTree) - .pipe(mergeMap((newSegments: UrlSegment[]) => { - return this.processSegment( - injector, routes, segmentGroup, newSegments.concat(remainingSegments), outlet, false); - })); + return this.applyRedirects.lineralizeSegments(route, newTree).pipe( + mergeMap((newSegments: UrlSegment[]) => { + return this.processSegment( + injector, + routes, + segmentGroup, + newSegments.concat(remainingSegments), + outlet, + false, + ); + }), + ); } matchSegmentAgainstRoute( - injector: EnvironmentInjector, rawSegment: UrlSegmentGroup, route: Route, - segments: UrlSegment[], outlet: string): Observable> { + injector: EnvironmentInjector, + rawSegment: UrlSegmentGroup, + route: Route, + segments: UrlSegment[], + outlet: string, + ): Observable> { const matchResult = matchWithChecks(rawSegment, route, segments, injector, this.urlSerializer); if (route.path === '**') { // Prior versions of the route matching algorithm would stop matching at the wildcard route. @@ -276,34 +383,47 @@ export class Recognizer { rawSegment.children = {}; } - return matchResult.pipe(switchMap((result) => { - if (!result.matched) { - return noMatch(rawSegment); - } + return matchResult.pipe( + switchMap((result) => { + if (!result.matched) { + return noMatch(rawSegment); + } - // If the route has an injector created from providers, we should start using that. - injector = route._injector ?? injector; - return this.getChildConfig(injector, route, segments) - .pipe(switchMap(({routes: childConfig}) => { + // If the route has an injector created from providers, we should start using that. + injector = route._injector ?? injector; + return this.getChildConfig(injector, route, segments).pipe( + switchMap(({routes: childConfig}) => { const childInjector = route._loadedInjector ?? injector; const {consumedSegments, remainingSegments, parameters} = result; const snapshot = new ActivatedRouteSnapshot( - consumedSegments, parameters, Object.freeze({...this.urlTree.queryParams}), - this.urlTree.fragment, getData(route), getOutlet(route), - route.component ?? route._loadedComponent ?? null, route, getResolve(route)); + consumedSegments, + parameters, + Object.freeze({...this.urlTree.queryParams}), + this.urlTree.fragment, + getData(route), + getOutlet(route), + route.component ?? route._loadedComponent ?? null, + route, + getResolve(route), + ); - const {segmentGroup, slicedSegments} = - split(rawSegment, consumedSegments, remainingSegments, childConfig); + const {segmentGroup, slicedSegments} = split( + rawSegment, + consumedSegments, + remainingSegments, + childConfig, + ); if (slicedSegments.length === 0 && segmentGroup.hasChildren()) { - return this.processChildren(childInjector, childConfig, segmentGroup) - .pipe(map(children => { - if (children === null) { - return null; - } - return new TreeNode(snapshot, children); - })); + return this.processChildren(childInjector, childConfig, segmentGroup).pipe( + map((children) => { + if (children === null) { + return null; + } + return new TreeNode(snapshot, children); + }), + ); } if (childConfig.length === 0 && slicedSegments.length === 0) { @@ -319,18 +439,28 @@ export class Recognizer { // {path: 'c', component: C}, // ]} // Notice that the children of the named outlet are configured with the primary outlet - return this - .processSegment( - childInjector, childConfig, segmentGroup, slicedSegments, - matchedOnOutlet ? PRIMARY_OUTLET : outlet, true) - .pipe(map(child => { - return new TreeNode(snapshot, child instanceof TreeNode ? [child] : []); - })); - })); - })); + return this.processSegment( + childInjector, + childConfig, + segmentGroup, + slicedSegments, + matchedOnOutlet ? PRIMARY_OUTLET : outlet, + true, + ).pipe( + map((child) => { + return new TreeNode(snapshot, child instanceof TreeNode ? [child] : []); + }), + ); + }), + ); + }), + ); } - private getChildConfig(injector: EnvironmentInjector, route: Route, segments: UrlSegment[]): - Observable { + private getChildConfig( + injector: EnvironmentInjector, + route: Route, + segments: UrlSegment[], + ): Observable { if (route.children) { // The children belong to the same module return of({routes: route.children, injector}); @@ -342,17 +472,19 @@ export class Recognizer { return of({routes: route._loadedRoutes, injector: route._loadedInjector}); } - return runCanLoadGuards(injector, route, segments, this.urlSerializer) - .pipe(mergeMap((shouldLoadResult: boolean) => { - if (shouldLoadResult) { - return this.configLoader.loadChildren(injector, route) - .pipe(tap((cfg: LoadedRouterConfig) => { - route._loadedRoutes = cfg.routes; - route._loadedInjector = cfg.injector; - })); - } - return canLoadFails(route); - })); + return runCanLoadGuards(injector, route, segments, this.urlSerializer).pipe( + mergeMap((shouldLoadResult: boolean) => { + if (shouldLoadResult) { + return this.configLoader.loadChildren(injector, route).pipe( + tap((cfg: LoadedRouterConfig) => { + route._loadedRoutes = cfg.routes; + route._loadedInjector = cfg.injector; + }), + ); + } + return canLoadFails(route); + }), + ); } return of({routes: [], injector}); @@ -377,8 +509,9 @@ function hasEmptyPathConfig(node: TreeNode) { * the children from each duplicate. This is necessary because different outlets can match a * single empty path route config and the results need to then be merged. */ -function mergeEmptyPathMatches(nodes: Array>): - Array> { +function mergeEmptyPathMatches( + nodes: Array>, +): Array> { const result: Array> = []; // The set of nodes which contain children that were merged from two duplicate empty path nodes. const mergedNodes: Set> = new Set(); @@ -389,8 +522,9 @@ function mergeEmptyPathMatches(nodes: Array>): continue; } - const duplicateEmptyPathNode = - result.find(resultNode => node.value.routeConfig === resultNode.value.routeConfig); + const duplicateEmptyPathNode = result.find( + (resultNode) => node.value.routeConfig === resultNode.value.routeConfig, + ); if (duplicateEmptyPathNode !== undefined) { duplicateEmptyPathNode.children.push(...node.children); mergedNodes.add(duplicateEmptyPathNode); @@ -406,20 +540,21 @@ function mergeEmptyPathMatches(nodes: Array>): const mergedChildren = mergeEmptyPathMatches(mergedNode.children); result.push(new TreeNode(mergedNode.value, mergedChildren)); } - return result.filter(n => !mergedNodes.has(n)); + return result.filter((n) => !mergedNodes.has(n)); } function checkOutletNameUniqueness(nodes: TreeNode[]): void { const names: {[k: string]: ActivatedRouteSnapshot} = {}; - nodes.forEach(n => { + nodes.forEach((n) => { const routeWithSameOutletName = names[n.value.outlet]; if (routeWithSameOutletName) { - const p = routeWithSameOutletName.url.map(s => s.toString()).join('/'); - const c = n.value.url.map(s => s.toString()).join('/'); + const p = routeWithSameOutletName.url.map((s) => s.toString()).join('/'); + const c = n.value.url.map((s) => s.toString()).join('/'); throw new RuntimeError( - RuntimeErrorCode.TWO_SEGMENTS_WITH_SAME_OUTLET, - (typeof ngDevMode === 'undefined' || ngDevMode) && - `Two segments cannot have the same outlet name: '${p}' and '${c}'.`); + RuntimeErrorCode.TWO_SEGMENTS_WITH_SAME_OUTLET, + (typeof ngDevMode === 'undefined' || ngDevMode) && + `Two segments cannot have the same outlet name: '${p}' and '${c}'.`, + ); } names[n.value.outlet] = n.value; }); diff --git a/packages/router/src/route_reuse_strategy.ts b/packages/router/src/route_reuse_strategy.ts index 82712cf9493..928ab289a83 100644 --- a/packages/router/src/route_reuse_strategy.ts +++ b/packages/router/src/route_reuse_strategy.ts @@ -26,9 +26,9 @@ export type DetachedRouteHandle = {}; /** @internal */ export type DetachedRouteHandleInternal = { - contexts: Map, - componentRef: ComponentRef, - route: TreeNode, + contexts: Map; + componentRef: ComponentRef; + route: TreeNode; }; /** @@ -48,13 +48,13 @@ export abstract class RouteReuseStrategy { * * Storing a `null` value should erase the previously stored value. */ - abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle|null): void; + abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void; /** Determines if this route (and its subtree) should be reattached */ abstract shouldAttach(route: ActivatedRouteSnapshot): boolean; /** Retrieves the previously stored route */ - abstract retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle|null; + abstract retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null; /** Determines if a route should be reused */ abstract shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean; @@ -97,7 +97,7 @@ export abstract class BaseRouteReuseStrategy implements RouteReuseStrategy { } /** Returns `null` because this strategy does not store routes for later re-use. */ - retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle|null { + retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null { return null; } @@ -112,5 +112,4 @@ export abstract class BaseRouteReuseStrategy implements RouteReuseStrategy { } @Injectable({providedIn: 'root'}) -export class DefaultRouteReuseStrategy extends BaseRouteReuseStrategy { -} +export class DefaultRouteReuseStrategy extends BaseRouteReuseStrategy {} diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index 9ee397af677..cd332ab074c 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -7,27 +7,57 @@ */ import {Location} from '@angular/common'; -import {inject, Injectable, NgZone, Type, ɵConsole as Console, ɵPendingTasks as PendingTasks, ɵRuntimeError as RuntimeError} from '@angular/core'; +import { + inject, + Injectable, + NgZone, + Type, + ɵConsole as Console, + ɵPendingTasks as PendingTasks, + ɵRuntimeError as RuntimeError, +} from '@angular/core'; import {Observable, Subject, Subscription, SubscriptionLike} from 'rxjs'; import {createSegmentGroupFromRoute, createUrlTreeFromSegmentGroup} from './create_url_tree'; import {INPUT_BINDER} from './directives/router_outlet'; import {RuntimeErrorCode} from './errors'; -import {BeforeActivateRoutes, Event, IMPERATIVE_NAVIGATION, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationTrigger, PrivateRouterEvents, RedirectRequest} from './events'; +import { + BeforeActivateRoutes, + Event, + IMPERATIVE_NAVIGATION, + NavigationCancel, + NavigationCancellationCode, + NavigationEnd, + NavigationTrigger, + PrivateRouterEvents, + RedirectRequest, +} from './events'; import {NavigationBehaviorOptions, OnSameUrlNavigation, Routes} from './models'; -import {isBrowserTriggeredNavigation, Navigation, NavigationExtras, NavigationTransitions, RestoredState, UrlCreationOptions} from './navigation_transition'; +import { + isBrowserTriggeredNavigation, + Navigation, + NavigationExtras, + NavigationTransitions, + RestoredState, + UrlCreationOptions, +} from './navigation_transition'; import {RouteReuseStrategy} from './route_reuse_strategy'; import {ROUTER_CONFIGURATION} from './router_config'; import {ROUTES} from './router_config_loader'; import {Params} from './shared'; import {StateManager} from './statemanager/state_manager'; import {UrlHandlingStrategy} from './url_handling_strategy'; -import {containsTree, IsActiveMatchOptions, isUrlTree, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree'; +import { + containsTree, + IsActiveMatchOptions, + isUrlTree, + UrlSegmentGroup, + UrlSerializer, + UrlTree, +} from './url_tree'; import {standardizeConfig, validateConfig} from './utils/config'; import {afterNextNavigation} from './utils/navigations'; - - function defaultErrorHandler(error: any): never { throw error; } @@ -40,7 +70,7 @@ export const exactMatchOptions: IsActiveMatchOptions = { paths: 'exact', fragment: 'ignored', matrixParams: 'ignored', - queryParams: 'exact' + queryParams: 'exact', }; /** @@ -51,7 +81,7 @@ export const subsetMatchOptions: IsActiveMatchOptions = { paths: 'subset', fragment: 'ignored', matrixParams: 'ignored', - queryParams: 'subset' + queryParams: 'subset', }; /** @@ -160,26 +190,29 @@ export class Router { this.resetConfig(this.config); - this.navigationTransitions.setupNavigations(this, this.currentUrlTree, this.routerState) - .subscribe({ - error: (e) => { - this.console.warn(ngDevMode ? `Unhandled Navigation Error: ${e}` : e); - } - }); + this.navigationTransitions + .setupNavigations(this, this.currentUrlTree, this.routerState) + .subscribe({ + error: (e) => { + this.console.warn(ngDevMode ? `Unhandled Navigation Error: ${e}` : e); + }, + }); this.subscribeToNavigationEvents(); } - private eventsSubscription = new Subscription(); private subscribeToNavigationEvents() { - const subscription = this.navigationTransitions.events.subscribe(e => { + const subscription = this.navigationTransitions.events.subscribe((e) => { try { const currentTransition = this.navigationTransitions.currentTransition; const currentNavigation = this.navigationTransitions.currentNavigation; if (currentTransition !== null && currentNavigation !== null) { this.stateManager.handleRouterEvent(e, currentNavigation); - if (e instanceof NavigationCancel && e.code !== NavigationCancellationCode.Redirect && - e.code !== NavigationCancellationCode.SupersededByNewNavigation) { + if ( + e instanceof NavigationCancel && + e.code !== NavigationCancellationCode.Redirect && + e.code !== NavigationCancellationCode.SupersededByNewNavigation + ) { // It seems weird that `navigated` is set to `true` when the navigation is rejected, // however it's how things were written initially. Investigation would need to be done // to determine if this can be removed. @@ -187,8 +220,10 @@ export class Router { } else if (e instanceof NavigationEnd) { this.navigated = true; } else if (e instanceof RedirectRequest) { - const mergedTree = - this.urlHandlingStrategy.merge(e.url, currentTransition.currentRawUrl); + const mergedTree = this.urlHandlingStrategy.merge( + e.url, + currentTransition.currentRawUrl, + ); const extras = { // Persist transient navigation info from the original navigation request. info: currentTransition.extras.info, @@ -197,14 +232,15 @@ export class Router { // updates or if the navigation was triggered by the browser (back // button, URL bar, etc). We want to replace that item in history // if the navigation is rejected. - replaceUrl: this.urlUpdateStrategy === 'eager' || - isBrowserTriggeredNavigation(currentTransition.source) + replaceUrl: + this.urlUpdateStrategy === 'eager' || + isBrowserTriggeredNavigation(currentTransition.source), }; this.scheduleNavigation(mergedTree, IMPERATIVE_NAVIGATION, null, extras, { resolve: currentTransition.resolve, reject: currentTransition.reject, - promise: currentTransition.promise + promise: currentTransition.promise, }); } } @@ -236,7 +272,10 @@ export class Router { this.setUpLocationChangeListener(); if (!this.navigationTransitions.hasRequestedNavigation) { this.navigateToSyncWithBrowser( - this.location.path(true), IMPERATIVE_NAVIGATION, this.stateManager.restoredState()); + this.location.path(true), + IMPERATIVE_NAVIGATION, + this.stateManager.restoredState(), + ); } } @@ -250,13 +289,13 @@ export class Router { // already patch onPopState, so location change callback will // run into ngZone this.nonRouterCurrentEntryChangeSubscription ??= - this.stateManager.registerNonRouterCurrentEntryChangeListener((url, state) => { - // The `setTimeout` was added in #12160 and is likely to support Angular/AngularJS - // hybrid apps. - setTimeout(() => { - this.navigateToSyncWithBrowser(url, 'popstate', state); - }, 0); - }); + this.stateManager.registerNonRouterCurrentEntryChangeListener((url, state) => { + // The `setTimeout` was added in #12160 and is likely to support Angular/AngularJS + // hybrid apps. + setTimeout(() => { + this.navigateToSyncWithBrowser(url, 'popstate', state); + }, 0); + }); } /** @@ -267,7 +306,10 @@ export class Router { * the Router needs to respond to ensure its internal state matches. */ private navigateToSyncWithBrowser( - url: string, source: NavigationTrigger, state: RestoredState|null|undefined) { + url: string, + source: NavigationTrigger, + state: RestoredState | null | undefined, + ) { const extras: NavigationExtras = {replaceUrl: true}; // TODO: restoredState should always include the entire state, regardless @@ -304,7 +346,7 @@ export class Router { * Returns the current `Navigation` object when the router is navigating, * and `null` when idle. */ - getCurrentNavigation(): Navigation|null { + getCurrentNavigation(): Navigation | null { return this.navigationTransitions.currentNavigation; } @@ -312,7 +354,7 @@ export class Router { * The `Navigation` object of the most recent navigation to succeed and `null` if there * has not been a successful navigation yet. */ - get lastSuccessfulNavigation(): Navigation|null { + get lastSuccessfulNavigation(): Navigation | null { return this.navigationTransitions.lastSuccessfulNavigation; } @@ -404,9 +446,9 @@ export class Router { */ createUrlTree(commands: any[], navigationExtras: UrlCreationOptions = {}): UrlTree { const {relativeTo, queryParams, fragment, queryParamsHandling, preserveFragment} = - navigationExtras; + navigationExtras; const f = preserveFragment ? this.currentUrlTree.fragment : fragment; - let q: Params|null = null; + let q: Params | null = null; switch (queryParamsHandling) { case 'merge': q = {...this.currentUrlTree.queryParams, ...queryParams}; @@ -421,7 +463,7 @@ export class Router { q = this.removeEmptyProps(q); } - let relativeToUrlSegmentGroup: UrlSegmentGroup|undefined; + let relativeToUrlSegmentGroup: UrlSegmentGroup | undefined; try { const relativeToSnapshot = relativeTo ? relativeTo.snapshot : this.routerState.snapshot.root; relativeToUrlSegmentGroup = createSegmentGroupFromRoute(relativeToSnapshot); @@ -470,13 +512,17 @@ export class Router { * @see [Routing and Navigation guide](guide/router) * */ - navigateByUrl(url: string|UrlTree, extras: NavigationBehaviorOptions = { - skipLocationChange: false - }): Promise { + navigateByUrl( + url: string | UrlTree, + extras: NavigationBehaviorOptions = { + skipLocationChange: false, + }, + ): Promise { if (typeof ngDevMode === 'undefined' || ngDevMode) { if (this.isNgZoneEnabled && !NgZone.isInAngularZone()) { this.console.warn( - `Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?`); + `Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?`, + ); } } @@ -516,8 +562,10 @@ export class Router { * @see [Routing and Navigation guide](guide/router) * */ - navigate(commands: any[], extras: NavigationExtras = {skipLocationChange: false}): - Promise { + navigate( + commands: any[], + extras: NavigationExtras = {skipLocationChange: false}, + ): Promise { validateCommands(commands); return this.navigateByUrl(this.createUrlTree(commands, extras), extras); } @@ -547,14 +595,14 @@ export class Router { * - The equivalent for `false` is * `{paths: 'subset', queryParams: 'subset', fragment: 'ignored', matrixParams: 'ignored'}`. */ - isActive(url: string|UrlTree, exact: boolean): boolean; + isActive(url: string | UrlTree, exact: boolean): boolean; /** * Returns whether the url is activated. */ - isActive(url: string|UrlTree, matchOptions: IsActiveMatchOptions): boolean; + isActive(url: string | UrlTree, matchOptions: IsActiveMatchOptions): boolean; /** @internal */ - isActive(url: string|UrlTree, matchOptions: boolean|IsActiveMatchOptions): boolean; - isActive(url: string|UrlTree, matchOptions: boolean|IsActiveMatchOptions): boolean { + isActive(url: string | UrlTree, matchOptions: boolean | IsActiveMatchOptions): boolean; + isActive(url: string | UrlTree, matchOptions: boolean | IsActiveMatchOptions): boolean { let options: IsActiveMatchOptions; if (matchOptions === true) { options = {...exactMatchOptions}; @@ -581,9 +629,12 @@ export class Router { } private scheduleNavigation( - rawUrl: UrlTree, source: NavigationTrigger, restoredState: RestoredState|null, - extras: NavigationExtras, - priorPromise?: {resolve: any, reject: any, promise: Promise}): Promise { + rawUrl: UrlTree, + source: NavigationTrigger, + restoredState: RestoredState | null, + extras: NavigationExtras, + priorPromise?: {resolve: any; reject: any; promise: Promise}, + ): Promise { if (this.disposed) { return Promise.resolve(false); } @@ -621,7 +672,7 @@ export class Router { reject, promise, currentSnapshot: this.routerState.snapshot, - currentRouterState: this.routerState + currentRouterState: this.routerState, }); // Make sure that the error is propagated even though `processNavigations` catch @@ -637,13 +688,14 @@ function validateCommands(commands: string[]): void { const cmd = commands[i]; if (cmd == null) { throw new RuntimeError( - RuntimeErrorCode.NULLISH_COMMAND, - (typeof ngDevMode === 'undefined' || ngDevMode) && - `The requested path contains ${cmd} segment at index ${i}`); + RuntimeErrorCode.NULLISH_COMMAND, + (typeof ngDevMode === 'undefined' || ngDevMode) && + `The requested path contains ${cmd} segment at index ${i}`, + ); } } } -function isPublicRouterEvent(e: Event|PrivateRouterEvents): e is Event { - return (!(e instanceof BeforeActivateRoutes) && !(e instanceof RedirectRequest)); +function isPublicRouterEvent(e: Event | PrivateRouterEvents): e is Event { + return !(e instanceof BeforeActivateRoutes) && !(e instanceof RedirectRequest); } diff --git a/packages/router/src/router_config.ts b/packages/router/src/router_config.ts index 7f71b59105b..9ca33807370 100644 --- a/packages/router/src/router_config.ts +++ b/packages/router/src/router_config.ts @@ -10,7 +10,6 @@ import {InjectionToken} from '@angular/core'; import {OnSameUrlNavigation} from './models'; - /** * Error handler that is invoked when a navigation error occurs. * @@ -46,7 +45,7 @@ export type ErrorHandler = (error: any) => any; * * @publicApi */ -export type InitialNavigation = 'disabled'|'enabledBlocking'|'enabledNonBlocking'; +export type InitialNavigation = 'disabled' | 'enabledBlocking' | 'enabledNonBlocking'; /** * Extra configuration options that can be used with the `withRouterConfig` function. @@ -75,7 +74,7 @@ export interface RouterConfigOptions { * * The default value is `replace` when not set. */ - canceledNavigationResolution?: 'replace'|'computed'; + canceledNavigationResolution?: 'replace' | 'computed'; /** * Configures the default for handling a navigation request to the current URL. @@ -103,7 +102,7 @@ export interface RouterConfigOptions { * `a;foo=bar/b`. * */ - paramsInheritanceStrategy?: 'emptyOnly'|'always'; + paramsInheritanceStrategy?: 'emptyOnly' | 'always'; /** * Defines when the router updates the browser URL. By default ('deferred'), @@ -112,7 +111,7 @@ export interface RouterConfigOptions { * Updating the URL early allows you to handle a failure of navigation by * showing an error message with the URL that failed. */ - urlUpdateStrategy?: 'deferred'|'eager'; + urlUpdateStrategy?: 'deferred' | 'eager'; /** * When `true`, the `Promise` will instead resolve with `false`, as it does with other failed @@ -138,7 +137,7 @@ export interface InMemoryScrollingOptions { * Anchor scrolling does not happen on 'popstate'. Instead, we restore the position * that we stored or scroll to the top. */ - anchorScrolling?: 'disabled'|'enabled'; + anchorScrolling?: 'disabled' | 'enabled'; /** * Configures if the scroll position needs to be restored when navigating back. @@ -174,7 +173,7 @@ export interface InMemoryScrollingOptions { * } * ``` */ - scrollPositionRestoration?: 'disabled'|'enabled'|'top'; + scrollPositionRestoration?: 'disabled' | 'enabled' | 'top'; } /** @@ -255,7 +254,7 @@ export interface ExtraOptions extends InMemoryScrollingOptions, RouterConfigOpti * When given a function, the router invokes the function every time * it restores scroll position. */ - scrollOffset?: [number, number]|(() => [number, number]); + scrollOffset?: [number, number] | (() => [number, number]); } /** @@ -264,7 +263,9 @@ export interface ExtraOptions extends InMemoryScrollingOptions, RouterConfigOpti * @publicApi */ export const ROUTER_CONFIGURATION = new InjectionToken( - (typeof ngDevMode === 'undefined' || ngDevMode) ? 'router config' : '', { - providedIn: 'root', - factory: () => ({}), - }); + typeof ngDevMode === 'undefined' || ngDevMode ? 'router config' : '', + { + providedIn: 'root', + factory: () => ({}), + }, +); diff --git a/packages/router/src/router_config_loader.ts b/packages/router/src/router_config_loader.ts index 362be582327..8674506fff1 100644 --- a/packages/router/src/router_config_loader.ts +++ b/packages/router/src/router_config_loader.ts @@ -6,7 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -import {Compiler, EnvironmentInjector, inject, Injectable, InjectionToken, Injector, NgModuleFactory, Type} from '@angular/core'; +import { + Compiler, + EnvironmentInjector, + inject, + Injectable, + InjectionToken, + Injector, + NgModuleFactory, + Type, +} from '@angular/core'; import {ConnectableObservable, from, Observable, of, Subject} from 'rxjs'; import {finalize, map, mergeMap, refCount, tap} from 'rxjs/operators'; @@ -14,8 +23,6 @@ import {DefaultExport, LoadedRouterConfig, Route, Routes} from './models'; import {wrapIntoObservable} from './utils/collection'; import {assertStandalone, standardizeConfig, validateConfig} from './utils/config'; - - /** * The [DI token](guide/glossary/#di-token) for a router configuration. * @@ -48,24 +55,24 @@ export class RouterConfigLoader { if (this.onLoadStartListener) { this.onLoadStartListener(route); } - const loadRunner = wrapIntoObservable(route.loadComponent!()) - .pipe( - map(maybeUnwrapDefaultExport), - tap(component => { - if (this.onLoadEndListener) { - this.onLoadEndListener(route); - } - (typeof ngDevMode === 'undefined' || ngDevMode) && - assertStandalone(route.path ?? '', component); - route._loadedComponent = component; - }), - finalize(() => { - this.componentLoaders.delete(route); - }), - ); + const loadRunner = wrapIntoObservable(route.loadComponent!()).pipe( + map(maybeUnwrapDefaultExport), + tap((component) => { + if (this.onLoadEndListener) { + this.onLoadEndListener(route); + } + (typeof ngDevMode === 'undefined' || ngDevMode) && + assertStandalone(route.path ?? '', component); + route._loadedComponent = component; + }), + finalize(() => { + this.componentLoaders.delete(route); + }), + ); // Use custom ConnectableObservable as share in runners pipe increasing the bundle size too much - const loader = - new ConnectableObservable(loadRunner, () => new Subject>()).pipe(refCount()); + const loader = new ConnectableObservable(loadRunner, () => new Subject>()).pipe( + refCount(), + ); this.componentLoaders.set(route, loader); return loader; } @@ -80,16 +87,22 @@ export class RouterConfigLoader { if (this.onLoadStartListener) { this.onLoadStartListener(route); } - const moduleFactoryOrRoutes$ = - loadChildren(route, this.compiler, parentInjector, this.onLoadEndListener); + const moduleFactoryOrRoutes$ = loadChildren( + route, + this.compiler, + parentInjector, + this.onLoadEndListener, + ); const loadRunner = moduleFactoryOrRoutes$.pipe( - finalize(() => { - this.childrenLoaders.delete(route); - }), + finalize(() => { + this.childrenLoaders.delete(route); + }), ); // Use custom ConnectableObservable as share in runners pipe increasing the bundle size too much - const loader = new ConnectableObservable(loadRunner, () => new Subject()) - .pipe(refCount()); + const loader = new ConnectableObservable( + loadRunner, + () => new Subject(), + ).pipe(refCount()); this.childrenLoaders.set(route, loader); return loader; } @@ -104,54 +117,56 @@ export class RouterConfigLoader { * an update to the extractor. */ export function loadChildren( - route: Route, compiler: Compiler, parentInjector: Injector, - onLoadEndListener?: (r: Route) => void): Observable { - return wrapIntoObservable(route.loadChildren!()) - .pipe( - map(maybeUnwrapDefaultExport), - mergeMap((t) => { - if (t instanceof NgModuleFactory || Array.isArray(t)) { - return of(t); - } else { - return from(compiler.compileModuleAsync(t)); - } - }), - map((factoryOrRoutes: NgModuleFactory|Routes) => { - if (onLoadEndListener) { - onLoadEndListener(route); - } - // This injector comes from the `NgModuleRef` when lazy loading an `NgModule`. There is - // no injector associated with lazy loading a `Route` array. - let injector: EnvironmentInjector|undefined; - let rawRoutes: Route[]; - let requireStandaloneComponents = false; - if (Array.isArray(factoryOrRoutes)) { - rawRoutes = factoryOrRoutes; - requireStandaloneComponents = true; - } else { - injector = factoryOrRoutes.create(parentInjector).injector; - // When loading a module that doesn't provide `RouterModule.forChild()` preloader - // will get stuck in an infinite loop. The child module's Injector will look to - // its parent `Injector` when it doesn't find any ROUTES so it will return routes - // for it's parent module instead. - rawRoutes = injector.get(ROUTES, [], {optional: true, self: true}).flat(); - } - const routes = rawRoutes.map(standardizeConfig); - (typeof ngDevMode === 'undefined' || ngDevMode) && - validateConfig(routes, route.path, requireStandaloneComponents); - return {routes, injector}; - }), - ); + route: Route, + compiler: Compiler, + parentInjector: Injector, + onLoadEndListener?: (r: Route) => void, +): Observable { + return wrapIntoObservable(route.loadChildren!()).pipe( + map(maybeUnwrapDefaultExport), + mergeMap((t) => { + if (t instanceof NgModuleFactory || Array.isArray(t)) { + return of(t); + } else { + return from(compiler.compileModuleAsync(t)); + } + }), + map((factoryOrRoutes: NgModuleFactory | Routes) => { + if (onLoadEndListener) { + onLoadEndListener(route); + } + // This injector comes from the `NgModuleRef` when lazy loading an `NgModule`. There is + // no injector associated with lazy loading a `Route` array. + let injector: EnvironmentInjector | undefined; + let rawRoutes: Route[]; + let requireStandaloneComponents = false; + if (Array.isArray(factoryOrRoutes)) { + rawRoutes = factoryOrRoutes; + requireStandaloneComponents = true; + } else { + injector = factoryOrRoutes.create(parentInjector).injector; + // When loading a module that doesn't provide `RouterModule.forChild()` preloader + // will get stuck in an infinite loop. The child module's Injector will look to + // its parent `Injector` when it doesn't find any ROUTES so it will return routes + // for it's parent module instead. + rawRoutes = injector.get(ROUTES, [], {optional: true, self: true}).flat(); + } + const routes = rawRoutes.map(standardizeConfig); + (typeof ngDevMode === 'undefined' || ngDevMode) && + validateConfig(routes, route.path, requireStandaloneComponents); + return {routes, injector}; + }), + ); } -function isWrappedDefaultExport(value: T|DefaultExport): value is DefaultExport { +function isWrappedDefaultExport(value: T | DefaultExport): value is DefaultExport { // We use `in` here with a string key `'default'`, because we expect `DefaultExport` objects to be // dynamically imported ES modules with a spec-mandated `default` key. Thus we don't expect that // `default` will be a renamed property. return value && typeof value === 'object' && 'default' in value; } -function maybeUnwrapDefaultExport(input: T|DefaultExport): T { +function maybeUnwrapDefaultExport(input: T | DefaultExport): T { // As per `isWrappedDefaultExport`, the `default` key here is generated by the browser and not // subject to property renaming, so we reference it with bracket access. return isWrappedDefaultExport(input) ? input['default'] : input; diff --git a/packages/router/src/router_module.ts b/packages/router/src/router_module.ts index 168f05650aa..4dfbb1868ba 100644 --- a/packages/router/src/router_module.ts +++ b/packages/router/src/router_module.ts @@ -6,8 +6,27 @@ * found in the LICENSE file at https://angular.io/license */ -import {HashLocationStrategy, Location, LocationStrategy, PathLocationStrategy, ViewportScroller} from '@angular/common'; -import {APP_BOOTSTRAP_LISTENER, ComponentRef, inject, Inject, InjectionToken, ModuleWithProviders, NgModule, NgZone, Optional, Provider, SkipSelf, ɵRuntimeError as RuntimeError} from '@angular/core'; +import { + HashLocationStrategy, + Location, + LocationStrategy, + PathLocationStrategy, + ViewportScroller, +} from '@angular/common'; +import { + APP_BOOTSTRAP_LISTENER, + ComponentRef, + inject, + Inject, + InjectionToken, + ModuleWithProviders, + NgModule, + NgZone, + Optional, + Provider, + SkipSelf, + ɵRuntimeError as RuntimeError, +} from '@angular/core'; import {EmptyOutletComponent} from './components/empty_outlet'; import {RouterLink} from './directives/router_link'; @@ -16,7 +35,17 @@ import {RouterOutlet} from './directives/router_outlet'; import {RuntimeErrorCode} from './errors'; import {Routes} from './models'; import {NavigationTransitions} from './navigation_transition'; -import {getBootstrapListener, rootRoute, ROUTER_IS_PROVIDED, withComponentInputBinding, withDebugTracing, withDisabledInitialNavigation, withEnabledBlockingInitialNavigation, withPreloading, withViewTransitions} from './provide_router'; +import { + getBootstrapListener, + rootRoute, + ROUTER_IS_PROVIDED, + withComponentInputBinding, + withDebugTracing, + withDisabledInitialNavigation, + withEnabledBlockingInitialNavigation, + withPreloading, + withViewTransitions, +} from './provide_router'; import {Router} from './router'; import {ExtraOptions, ROUTER_CONFIGURATION} from './router_config'; import {RouterConfigLoader, ROUTES} from './router_config_loader'; @@ -25,7 +54,6 @@ import {ROUTER_SCROLLER, RouterScroller} from './router_scroller'; import {ActivatedRoute} from './router_state'; import {DefaultUrlSerializer, UrlSerializer} from './url_tree'; - /** * The directives defined in the `RouterModule`. */ @@ -35,8 +63,10 @@ const ROUTER_DIRECTIVES = [RouterOutlet, RouterLink, RouterLinkActive, EmptyOutl * @docsNotRequired */ export const ROUTER_FORROOT_GUARD = new InjectionToken( - (typeof ngDevMode === 'undefined' || ngDevMode) ? 'router duplicate forRoot guard' : - 'ROUTER_FORROOT_GUARD'); + typeof ngDevMode === 'undefined' || ngDevMode + ? 'router duplicate forRoot guard' + : 'ROUTER_FORROOT_GUARD', +); // TODO(atscott): All of these except `ActivatedRoute` are `providedIn: 'root'`. They are only kept // here to avoid a breaking change whereby the provider order matters based on where the @@ -51,8 +81,9 @@ export const ROUTER_PROVIDERS: Provider[] = [ RouterConfigLoader, // Only used to warn when `provideRoutes` is used without `RouterModule` or `provideRouter`. Can // be removed when `provideRoutes` is removed. - (typeof ngDevMode === 'undefined' || ngDevMode) ? {provide: ROUTER_IS_PROVIDED, useValue: true} : - [], + typeof ngDevMode === 'undefined' || ngDevMode + ? {provide: ROUTER_IS_PROVIDED, useValue: true} + : [], ]; /** @@ -106,14 +137,16 @@ export class RouterModule { ngModule: RouterModule, providers: [ ROUTER_PROVIDERS, - (typeof ngDevMode === 'undefined' || ngDevMode) ? - (config?.enableTracing ? withDebugTracing().ɵproviders : []) : - [], + typeof ngDevMode === 'undefined' || ngDevMode + ? config?.enableTracing + ? withDebugTracing().ɵproviders + : [] + : [], {provide: ROUTES, multi: true, useValue: routes}, { provide: ROUTER_FORROOT_GUARD, useFactory: provideForRootGuard, - deps: [[Router, new Optional(), new SkipSelf()]] + deps: [[Router, new Optional(), new SkipSelf()]], }, {provide: ROUTER_CONFIGURATION, useValue: config ? config : {}}, config?.useHash ? provideHashLocationStrategy() : providePathLocationStrategy(), @@ -187,9 +220,10 @@ function providePathLocationStrategy(): Provider { export function provideForRootGuard(router: Router): any { if ((typeof ngDevMode === 'undefined' || ngDevMode) && router) { throw new RuntimeError( - RuntimeErrorCode.FOR_ROOT_CALLED_TWICE, - `The Router was provided more than once. This can happen if 'forRoot' is used outside of the root injector.` + - ` Lazy loaded modules should use RouterModule.forChild() instead.`); + RuntimeErrorCode.FOR_ROOT_CALLED_TWICE, + `The Router was provided more than once. This can happen if 'forRoot' is used outside of the root injector.` + + ` Lazy loaded modules should use RouterModule.forChild() instead.`, + ); } return 'guarded'; } @@ -199,9 +233,9 @@ export function provideForRootGuard(router: Router): any { function provideInitialNavigation(config: Pick): Provider[] { return [ config.initialNavigation === 'disabled' ? withDisabledInitialNavigation().ɵproviders : [], - config.initialNavigation === 'enabledBlocking' ? - withEnabledBlockingInitialNavigation().ɵproviders : - [], + config.initialNavigation === 'enabledBlocking' + ? withEnabledBlockingInitialNavigation().ɵproviders + : [], ]; } @@ -213,7 +247,8 @@ function provideInitialNavigation(config: Pick) => void>( - (typeof ngDevMode === 'undefined' || ngDevMode) ? 'Router Initializer' : ''); + typeof ngDevMode === 'undefined' || ngDevMode ? 'Router Initializer' : '', +); function provideRouterInitializer(): Provider[] { return [ diff --git a/packages/router/src/router_outlet_context.ts b/packages/router/src/router_outlet_context.ts index d12c66cb6f4..02d050bcdb6 100644 --- a/packages/router/src/router_outlet_context.ts +++ b/packages/router/src/router_outlet_context.ts @@ -11,18 +11,17 @@ import {ComponentRef, EnvironmentInjector, Injectable} from '@angular/core'; import {RouterOutletContract} from './directives/router_outlet'; import {ActivatedRoute} from './router_state'; - /** * Store contextual information about a `RouterOutlet` * * @publicApi */ export class OutletContext { - outlet: RouterOutletContract|null = null; - route: ActivatedRoute|null = null; - injector: EnvironmentInjector|null = null; + outlet: RouterOutletContract | null = null; + route: ActivatedRoute | null = null; + injector: EnvironmentInjector | null = null; children = new ChildrenOutletContexts(); - attachRef: ComponentRef|null = null; + attachRef: ComponentRef | null = null; } /** @@ -80,7 +79,7 @@ export class ChildrenOutletContexts { return context; } - getContext(childName: string): OutletContext|null { + getContext(childName: string): OutletContext | null { return this.contexts.get(childName) || null; } } diff --git a/packages/router/src/router_preloader.ts b/packages/router/src/router_preloader.ts index 79cf0b85526..0f1c11dde33 100644 --- a/packages/router/src/router_preloader.ts +++ b/packages/router/src/router_preloader.ts @@ -6,7 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {Compiler, createEnvironmentInjector, EnvironmentInjector, Injectable, OnDestroy} from '@angular/core'; +import { + Compiler, + createEnvironmentInjector, + EnvironmentInjector, + Injectable, + OnDestroy, +} from '@angular/core'; import {from, Observable, of, Subscription} from 'rxjs'; import {catchError, concatMap, filter, mergeAll, mergeMap} from 'rxjs/operators'; @@ -15,7 +21,6 @@ import {LoadedRouterConfig, Route, Routes} from './models'; import {Router} from './router'; import {RouterConfigLoader} from './router_config_loader'; - /** * @description * @@ -78,14 +83,20 @@ export class RouterPreloader implements OnDestroy { private subscription?: Subscription; constructor( - private router: Router, compiler: Compiler, private injector: EnvironmentInjector, - private preloadingStrategy: PreloadingStrategy, private loader: RouterConfigLoader) {} + private router: Router, + compiler: Compiler, + private injector: EnvironmentInjector, + private preloadingStrategy: PreloadingStrategy, + private loader: RouterConfigLoader, + ) {} setUpPreloading(): void { - this.subscription = - this.router.events - .pipe(filter((e: Event) => e instanceof NavigationEnd), concatMap(() => this.preload())) - .subscribe(() => {}); + this.subscription = this.router.events + .pipe( + filter((e: Event) => e instanceof NavigationEnd), + concatMap(() => this.preload()), + ) + .subscribe(() => {}); } preload(): Observable { @@ -103,8 +114,11 @@ export class RouterPreloader implements OnDestroy { const res: Observable[] = []; for (const route of routes) { if (route.providers && !route._injector) { - route._injector = - createEnvironmentInjector(route.providers, injector, `Route: ${route.path}`); + route._injector = createEnvironmentInjector( + route.providers, + injector, + `Route: ${route.path}`, + ); } const injectorForCurrentRoute = route._injector ?? injector; @@ -118,8 +132,10 @@ export class RouterPreloader implements OnDestroy { // when present. Lastly, it remains to be decided whether `canLoad` should behave this way // at all. Code splitting and lazy loading is separate from client-side authorization checks // and should not be used as a security measure to prevent loading of code. - if ((route.loadChildren && !route._loadedRoutes && route.canLoad === undefined) || - (route.loadComponent && !route._loadedComponent)) { + if ( + (route.loadChildren && !route._loadedRoutes && route.canLoad === undefined) || + (route.loadComponent && !route._loadedComponent) + ) { res.push(this.preloadConfig(injectorForCurrentRoute, route)); } if (route.children || route._loadedRoutes) { @@ -131,24 +147,25 @@ export class RouterPreloader implements OnDestroy { private preloadConfig(injector: EnvironmentInjector, route: Route): Observable { return this.preloadingStrategy.preload(route, () => { - let loadedChildren$: Observable; + let loadedChildren$: Observable; if (route.loadChildren && route.canLoad === undefined) { loadedChildren$ = this.loader.loadChildren(injector, route); } else { loadedChildren$ = of(null); } - const recursiveLoadChildren$ = - loadedChildren$.pipe(mergeMap((config: LoadedRouterConfig|null) => { - if (config === null) { - return of(void 0); - } - route._loadedRoutes = config.routes; - route._loadedInjector = config.injector; - // If the loaded config was a module, use that as the module/module injector going - // forward. Otherwise, continue using the current module/module injector. - return this.processRoutes(config.injector ?? injector, config.routes); - })); + const recursiveLoadChildren$ = loadedChildren$.pipe( + mergeMap((config: LoadedRouterConfig | null) => { + if (config === null) { + return of(void 0); + } + route._loadedRoutes = config.routes; + route._loadedInjector = config.injector; + // If the loaded config was a module, use that as the module/module injector going + // forward. Otherwise, continue using the current module/module injector. + return this.processRoutes(config.injector ?? injector, config.routes); + }), + ); if (route.loadComponent && !route._loadedComponent) { const loadComponent$ = this.loader.loadComponent(route); return from([recursiveLoadChildren$, loadComponent$]).pipe(mergeAll()); diff --git a/packages/router/src/router_scroller.ts b/packages/router/src/router_scroller.ts index ec621471039..8aee665c9d7 100644 --- a/packages/router/src/router_scroller.ts +++ b/packages/router/src/router_scroller.ts @@ -10,7 +10,13 @@ import {ViewportScroller} from '@angular/common'; import {Injectable, InjectionToken, NgZone, OnDestroy} from '@angular/core'; import {Unsubscribable} from 'rxjs'; -import {NavigationEnd, NavigationSkipped, NavigationSkippedCode, NavigationStart, Scroll} from './events'; +import { + NavigationEnd, + NavigationSkipped, + NavigationSkippedCode, + NavigationStart, + Scroll, +} from './events'; import {NavigationTransitions} from './navigation_transition'; import {UrlSerializer} from './url_tree'; @@ -22,18 +28,21 @@ export class RouterScroller implements OnDestroy { private scrollEventsSubscription?: Unsubscribable; private lastId = 0; - private lastSource: 'imperative'|'popstate'|'hashchange'|undefined = 'imperative'; + private lastSource: 'imperative' | 'popstate' | 'hashchange' | undefined = 'imperative'; private restoredId = 0; private store: {[key: string]: [number, number]} = {}; /** @nodoc */ constructor( - readonly urlSerializer: UrlSerializer, private transitions: NavigationTransitions, - public readonly viewportScroller: ViewportScroller, private readonly zone: NgZone, - private options: { - scrollPositionRestoration?: 'disabled'|'enabled'|'top', - anchorScrolling?: 'disabled'|'enabled' - } = {}) { + readonly urlSerializer: UrlSerializer, + private transitions: NavigationTransitions, + public readonly viewportScroller: ViewportScroller, + private readonly zone: NgZone, + private options: { + scrollPositionRestoration?: 'disabled' | 'enabled' | 'top'; + anchorScrolling?: 'disabled' | 'enabled'; + } = {}, + ) { // Default both options to 'disabled' options.scrollPositionRestoration ||= 'disabled'; options.anchorScrolling ||= 'disabled'; @@ -51,7 +60,7 @@ export class RouterScroller implements OnDestroy { } private createScrollEvents() { - return this.transitions.events.subscribe(e => { + return this.transitions.events.subscribe((e) => { if (e instanceof NavigationStart) { // store the scroll position of the current stable navigations. this.store[this.lastId] = this.viewportScroller.getScrollPosition(); @@ -61,8 +70,9 @@ export class RouterScroller implements OnDestroy { this.lastId = e.id; this.scheduleScrollEvent(e, this.urlSerializer.parse(e.urlAfterRedirects).fragment); } else if ( - e instanceof NavigationSkipped && - e.code === NavigationSkippedCode.IgnoredSameUrlNavigation) { + e instanceof NavigationSkipped && + e.code === NavigationSkippedCode.IgnoredSameUrlNavigation + ) { this.lastSource = undefined; this.restoredId = 0; this.scheduleScrollEvent(e, this.urlSerializer.parse(e.url).fragment); @@ -71,7 +81,7 @@ export class RouterScroller implements OnDestroy { } private consumeScrollEvents() { - return this.transitions.events.subscribe(e => { + return this.transitions.events.subscribe((e) => { if (!(e instanceof Scroll)) return; // a popstate event. The pop state event will always ignore anchor scrolling. if (e.position) { @@ -91,17 +101,23 @@ export class RouterScroller implements OnDestroy { }); } - private scheduleScrollEvent(routerEvent: NavigationEnd|NavigationSkipped, anchor: string|null): - void { + private scheduleScrollEvent( + routerEvent: NavigationEnd | NavigationSkipped, + anchor: string | null, + ): void { this.zone.runOutsideAngular(() => { // The scroll event needs to be delayed until after change detection. Otherwise, we may // attempt to restore the scroll position before the router outlet has fully rendered the // component by executing its update block of the template function. setTimeout(() => { this.zone.run(() => { - this.transitions.events.next(new Scroll( - routerEvent, this.lastSource === 'popstate' ? this.store[this.restoredId] : null, - anchor)); + this.transitions.events.next( + new Scroll( + routerEvent, + this.lastSource === 'popstate' ? this.store[this.restoredId] : null, + anchor, + ), + ); }); }, 0); }); diff --git a/packages/router/src/router_state.ts b/packages/router/src/router_state.ts index d5612c49a0e..9d9b26d6d9a 100644 --- a/packages/router/src/router_state.ts +++ b/packages/router/src/router_state.ts @@ -50,9 +50,10 @@ import {Tree, TreeNode} from './utils/tree'; export class RouterState extends Tree { /** @internal */ constructor( - root: TreeNode, - /** The current snapshot of the router state */ - public snapshot: RouterStateSnapshot) { + root: TreeNode, + /** The current snapshot of the router state */ + public snapshot: RouterStateSnapshot, + ) { super(root); setRouterState(this, root); } @@ -62,28 +63,43 @@ export class RouterState extends Tree { } } -export function createEmptyState(rootComponent: Type|null): RouterState { +export function createEmptyState(rootComponent: Type | null): RouterState { const snapshot = createEmptyStateSnapshot(rootComponent); const emptyUrl = new BehaviorSubject([new UrlSegment('', {})]); const emptyParams = new BehaviorSubject({}); const emptyData = new BehaviorSubject({}); const emptyQueryParams = new BehaviorSubject({}); - const fragment = new BehaviorSubject(''); + const fragment = new BehaviorSubject(''); const activated = new ActivatedRoute( - emptyUrl, emptyParams, emptyQueryParams, fragment, emptyData, PRIMARY_OUTLET, rootComponent, - snapshot.root); + emptyUrl, + emptyParams, + emptyQueryParams, + fragment, + emptyData, + PRIMARY_OUTLET, + rootComponent, + snapshot.root, + ); activated.snapshot = snapshot.root; return new RouterState(new TreeNode(activated, []), snapshot); } -export function createEmptyStateSnapshot(rootComponent: Type|null): RouterStateSnapshot { +export function createEmptyStateSnapshot(rootComponent: Type | null): RouterStateSnapshot { const emptyParams = {}; const emptyData = {}; const emptyQueryParams = {}; const fragment = ''; const activated = new ActivatedRouteSnapshot( - [], emptyParams, emptyQueryParams, fragment, emptyData, PRIMARY_OUTLET, rootComponent, null, - {}); + [], + emptyParams, + emptyQueryParams, + fragment, + emptyData, + PRIMARY_OUTLET, + rootComponent, + null, + {}, + ); return new RouterStateSnapshot('', new TreeNode(activated, [])); } @@ -119,7 +135,7 @@ export class ActivatedRoute { _queryParamMap?: Observable; /** An Observable of the resolved route title */ - readonly title: Observable; + readonly title: Observable; /** An observable of the URL segments matched by this route. */ public url: Observable; @@ -128,26 +144,28 @@ export class ActivatedRoute { /** An observable of the query parameters shared by all the routes. */ public queryParams: Observable; /** An observable of the URL fragment shared by all the routes. */ - public fragment: Observable; + public fragment: Observable; /** An observable of the static and resolved data of this route. */ public data: Observable; /** @internal */ constructor( - /** @internal */ - public urlSubject: BehaviorSubject, - /** @internal */ - public paramsSubject: BehaviorSubject, - /** @internal */ - public queryParamsSubject: BehaviorSubject, - /** @internal */ - public fragmentSubject: BehaviorSubject, - /** @internal */ - public dataSubject: BehaviorSubject, - /** The outlet name of the route, a constant. */ - public outlet: string, - /** The component of the route, a constant. */ - public component: Type|null, futureSnapshot: ActivatedRouteSnapshot) { + /** @internal */ + public urlSubject: BehaviorSubject, + /** @internal */ + public paramsSubject: BehaviorSubject, + /** @internal */ + public queryParamsSubject: BehaviorSubject, + /** @internal */ + public fragmentSubject: BehaviorSubject, + /** @internal */ + public dataSubject: BehaviorSubject, + /** The outlet name of the route, a constant. */ + public outlet: string, + /** The component of the route, a constant. */ + public component: Type | null, + futureSnapshot: ActivatedRouteSnapshot, + ) { this._futureSnapshot = futureSnapshot; this.title = this.dataSubject?.pipe(map((d: Data) => d[RouteTitleKey])) ?? of(undefined); // TODO(atscott): Verify that these can be changed to `.asObservable()` with TGP. @@ -159,7 +177,7 @@ export class ActivatedRoute { } /** The configuration used to match this route. */ - get routeConfig(): Route|null { + get routeConfig(): Route | null { return this._futureSnapshot.routeConfig; } @@ -169,12 +187,12 @@ export class ActivatedRoute { } /** The parent of this route in the router state tree. */ - get parent(): ActivatedRoute|null { + get parent(): ActivatedRoute | null { return this._routerState.parent(this); } /** The first child of this route in the router state tree. */ - get firstChild(): ActivatedRoute|null { + get firstChild(): ActivatedRoute | null { return this._routerState.firstChild(this); } @@ -203,8 +221,9 @@ export class ActivatedRoute { * The map supports retrieving single and multiple values from the query parameter. */ get queryParamMap(): Observable { - this._queryParamMap ??= - this.queryParams.pipe(map((p: Params): ParamMap => convertToParamMap(p))); + this._queryParamMap ??= this.queryParams.pipe( + map((p: Params): ParamMap => convertToParamMap(p)), + ); return this._queryParamMap; } @@ -213,13 +232,13 @@ export class ActivatedRoute { } } -export type ParamsInheritanceStrategy = 'emptyOnly'|'always'; +export type ParamsInheritanceStrategy = 'emptyOnly' | 'always'; /** @internal */ export type Inherited = { - params: Params, - data: Data, - resolve: Data, + params: Params; + data: Data; + resolve: Data; }; /** @@ -229,16 +248,20 @@ export type Inherited = { * route is component-less. */ export function getInherited( - route: ActivatedRouteSnapshot, parent: ActivatedRouteSnapshot|null, - paramsInheritanceStrategy: ParamsInheritanceStrategy = 'emptyOnly'): Inherited { + route: ActivatedRouteSnapshot, + parent: ActivatedRouteSnapshot | null, + paramsInheritanceStrategy: ParamsInheritanceStrategy = 'emptyOnly', +): Inherited { let inherited: Inherited; const {routeConfig} = route; - if (parent !== null && - (paramsInheritanceStrategy === 'always' || - // inherit parent data if route is empty path - routeConfig?.path === '' || - // inherit parent data if parent was componentless - (!parent.component && !parent.routeConfig?.loadComponent))) { + if ( + parent !== null && + (paramsInheritanceStrategy === 'always' || + // inherit parent data if route is empty path + routeConfig?.path === '' || + // inherit parent data if parent was componentless + (!parent.component && !parent.routeConfig?.loadComponent)) + ) { inherited = { params: {...parent.params, ...route.params}, data: {...parent.data, ...route.data}, @@ -256,13 +279,13 @@ export function getInherited( ...routeConfig?.data, // resolved data from current route overrides everything ...route._resolvedData, - } + }, }; } else { inherited = { params: {...route.params}, data: {...route.data}, - resolve: {...route.data, ...(route._resolvedData ?? {})} + resolve: {...route.data, ...(route._resolvedData ?? {})}, }; } @@ -297,7 +320,7 @@ export function getInherited( */ export class ActivatedRouteSnapshot { /** The configuration used to match this route **/ - public readonly routeConfig: Route|null; + public readonly routeConfig: Route | null; /** @internal */ _resolve: ResolveData; /** @internal */ @@ -310,7 +333,7 @@ export class ActivatedRouteSnapshot { _queryParamMap?: ParamMap; /** The resolved route title */ - get title(): string|undefined { + get title(): string | undefined { // Note: This _must_ be a getter because the data is mutated in the resolvers. Title will not be // available at the time of class instantiation. return this.data?.[RouteTitleKey]; @@ -318,38 +341,41 @@ export class ActivatedRouteSnapshot { /** @internal */ constructor( - /** The URL segments matched by this route */ - public url: UrlSegment[], - /** - * The matrix parameters scoped to this route. - * - * You can compute all params (or data) in the router state or to get params outside - * of an activated component by traversing the `RouterState` tree as in the following - * example: - * ``` - * collectRouteParams(router: Router) { - * let params = {}; - * let stack: ActivatedRouteSnapshot[] = [router.routerState.snapshot.root]; - * while (stack.length > 0) { - * const route = stack.pop()!; - * params = {...params, ...route.params}; - * stack.push(...route.children); - * } - * return params; - * } - * ``` - */ - public params: Params, - /** The query parameters shared by all the routes */ - public queryParams: Params, - /** The URL fragment shared by all the routes */ - public fragment: string|null, - /** The static and resolved data of this route */ - public data: Data, - /** The outlet name of the route */ - public outlet: string, - /** The component of the route */ - public component: Type|null, routeConfig: Route|null, resolve: ResolveData) { + /** The URL segments matched by this route */ + public url: UrlSegment[], + /** + * The matrix parameters scoped to this route. + * + * You can compute all params (or data) in the router state or to get params outside + * of an activated component by traversing the `RouterState` tree as in the following + * example: + * ``` + * collectRouteParams(router: Router) { + * let params = {}; + * let stack: ActivatedRouteSnapshot[] = [router.routerState.snapshot.root]; + * while (stack.length > 0) { + * const route = stack.pop()!; + * params = {...params, ...route.params}; + * stack.push(...route.children); + * } + * return params; + * } + * ``` + */ + public params: Params, + /** The query parameters shared by all the routes */ + public queryParams: Params, + /** The URL fragment shared by all the routes */ + public fragment: string | null, + /** The static and resolved data of this route */ + public data: Data, + /** The outlet name of the route */ + public outlet: string, + /** The component of the route */ + public component: Type | null, + routeConfig: Route | null, + resolve: ResolveData, + ) { this.routeConfig = routeConfig; this._resolve = resolve; } @@ -360,12 +386,12 @@ export class ActivatedRouteSnapshot { } /** The parent of this route in the router state tree */ - get parent(): ActivatedRouteSnapshot|null { + get parent(): ActivatedRouteSnapshot | null { return this._routerState.parent(this); } /** The first child of this route in the router state tree */ - get firstChild(): ActivatedRouteSnapshot|null { + get firstChild(): ActivatedRouteSnapshot | null { return this._routerState.firstChild(this); } @@ -390,7 +416,7 @@ export class ActivatedRouteSnapshot { } toString(): string { - const url = this.url.map(segment => segment.toString()).join('/'); + const url = this.url.map((segment) => segment.toString()).join('/'); const matched = this.routeConfig ? this.routeConfig.path : ''; return `Route(url:'${url}', path:'${matched}')`; } @@ -426,8 +452,10 @@ export class ActivatedRouteSnapshot { export class RouterStateSnapshot extends Tree { /** @internal */ constructor( - /** The url from which this snapshot was created */ - public url: string, root: TreeNode) { + /** The url from which this snapshot was created */ + public url: string, + root: TreeNode, + ) { super(root); setRouterState(this, root); } @@ -439,7 +467,7 @@ export class RouterStateSnapshot extends Tree { function setRouterState(state: U, node: TreeNode): void { node.value._routerState = state; - node.children.forEach(c => setRouterState(state, c)); + node.children.forEach((c) => setRouterState(state, c)); } function serializeNode(node: TreeNode): string { @@ -480,14 +508,18 @@ export function advanceActivatedRoute(route: ActivatedRoute): void { } } - export function equalParamsAndUrlSegments( - a: ActivatedRouteSnapshot, b: ActivatedRouteSnapshot): boolean { + a: ActivatedRouteSnapshot, + b: ActivatedRouteSnapshot, +): boolean { const equalUrlParams = shallowEqual(a.params, b.params) && equalSegments(a.url, b.url); const parentsMismatch = !a.parent !== !b.parent; - return equalUrlParams && !parentsMismatch && - (!a.parent || equalParamsAndUrlSegments(a.parent, b.parent!)); + return ( + equalUrlParams && + !parentsMismatch && + (!a.parent || equalParamsAndUrlSegments(a.parent, b.parent!)) + ); } export function hasStaticTitle(config: Route) { diff --git a/packages/router/src/shared.ts b/packages/router/src/shared.ts index 2cb8a04c6bc..2f534712d1a 100644 --- a/packages/router/src/shared.ts +++ b/packages/router/src/shared.ts @@ -9,7 +9,6 @@ import {Route, UrlMatchResult} from './models'; import {UrlSegment, UrlSegmentGroup} from './url_tree'; - /** * The primary routing outlet. * @@ -59,7 +58,7 @@ export interface ParamMap { * or the first value if the parameter has multiple values, * or `null` when there is no such parameter. */ - get(name: string): string|null; + get(name: string): string | null; /** * Retrieves multiple values for a parameter. * @param name The parameter name. @@ -84,7 +83,7 @@ class ParamsAsMap implements ParamMap { return Object.prototype.hasOwnProperty.call(this.params, name); } - get(name: string): string|null { + get(name: string): string | null { if (this.has(name)) { const v = this.params[name]; return Array.isArray(v) ? v[0] : v; @@ -134,7 +133,10 @@ export function convertToParamMap(params: Params): ParamMap { * @publicApi */ export function defaultUrlMatcher( - segments: UrlSegment[], segmentGroup: UrlSegmentGroup, route: Route): UrlMatchResult|null { + segments: UrlSegment[], + segmentGroup: UrlSegmentGroup, + route: Route, +): UrlMatchResult | null { const parts = route.path!.split('/'); if (parts.length > segments.length) { @@ -142,8 +144,10 @@ export function defaultUrlMatcher( return null; } - if (route.pathMatch === 'full' && - (segmentGroup.hasChildren() || parts.length < segments.length)) { + if ( + route.pathMatch === 'full' && + (segmentGroup.hasChildren() || parts.length < segments.length) + ) { // The config is longer than the actual URL but we are looking for a full match, return null return null; } diff --git a/packages/router/src/statemanager/state_manager.ts b/packages/router/src/statemanager/state_manager.ts index c34cfb67e17..da0ab026ada 100644 --- a/packages/router/src/statemanager/state_manager.ts +++ b/packages/router/src/statemanager/state_manager.ts @@ -10,7 +10,18 @@ import {Location} from '@angular/common'; import {inject, Injectable} from '@angular/core'; import {SubscriptionLike} from 'rxjs'; -import {BeforeActivateRoutes, Event, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationStart, PrivateRouterEvents, RoutesRecognized,} from '../events'; +import { + BeforeActivateRoutes, + Event, + NavigationCancel, + NavigationCancellationCode, + NavigationEnd, + NavigationError, + NavigationSkipped, + NavigationStart, + PrivateRouterEvents, + RoutesRecognized, +} from '../events'; import {Navigation, RestoredState} from '../navigation_transition'; import {ROUTER_CONFIGURATION} from '../router_config'; import {createEmptyState, RouterState} from '../router_state'; @@ -58,7 +69,7 @@ export abstract class StateManager { abstract getRawUrlTree(): UrlTree; /** Returns the current state stored by the browser for the current history entry. */ - abstract restoredState(): RestoredState|null|undefined; + abstract restoredState(): RestoredState | null | undefined; /** Returns the current RouterState. */ abstract getRouterState(): RouterState; @@ -69,13 +80,14 @@ export abstract class StateManager { * also includes programmatic APIs called by non-Router JavaScript. */ abstract registerNonRouterCurrentEntryChangeListener( - listener: (url: string, state: RestoredState|null|undefined) => void): SubscriptionLike; + listener: (url: string, state: RestoredState | null | undefined) => void, + ): SubscriptionLike; /** * Handles a navigation event sent from the Router. These are typically events that indicate a * navigation has started, progressed, been cancelled, or finished. */ - abstract handleRouterEvent(e: Event|PrivateRouterEvents, currentTransition: Navigation): void; + abstract handleRouterEvent(e: Event | PrivateRouterEvents, currentTransition: Navigation): void; } @Injectable({providedIn: 'root'}) @@ -84,7 +96,7 @@ export class HistoryStateManager extends StateManager { private readonly urlSerializer = inject(UrlSerializer); private readonly options = inject(ROUTER_CONFIGURATION, {optional: true}) || {}; private readonly canceledNavigationResolution = - this.options.canceledNavigationResolution || 'replace'; + this.options.canceledNavigationResolution || 'replace'; private urlHandlingStrategy = inject(UrlHandlingStrategy); private urlUpdateStrategy = this.options.urlUpdateStrategy || 'deferred'; @@ -112,7 +124,7 @@ export class HistoryStateManager extends StateManager { private currentPageId: number = 0; private lastSuccessfulId: number = -1; - override restoredState(): RestoredState|null|undefined { + override restoredState(): RestoredState | null | undefined { return this.location.getState() as RestoredState | null | undefined; } @@ -145,15 +157,16 @@ export class HistoryStateManager extends StateManager { } override registerNonRouterCurrentEntryChangeListener( - listener: (url: string, state: RestoredState|null|undefined) => void): SubscriptionLike { - return this.location.subscribe(event => { + listener: (url: string, state: RestoredState | null | undefined) => void, + ): SubscriptionLike { + return this.location.subscribe((event) => { if (event['type'] === 'popstate') { listener(event['url']!, event.state as RestoredState | null | undefined); } }); } - override handleRouterEvent(e: Event|PrivateRouterEvents, currentTransition: Navigation) { + override handleRouterEvent(e: Event | PrivateRouterEvents, currentTransition: Navigation) { if (e instanceof NavigationStart) { this.stateMemento = this.createStateMemento(); } else if (e instanceof NavigationSkipped) { @@ -162,14 +175,18 @@ export class HistoryStateManager extends StateManager { if (this.urlUpdateStrategy === 'eager') { if (!currentTransition.extras.skipLocationChange) { const rawUrl = this.urlHandlingStrategy.merge( - currentTransition.finalUrl!, currentTransition.initialUrl); + currentTransition.finalUrl!, + currentTransition.initialUrl, + ); this.setBrowserUrl(rawUrl, currentTransition); } } } else if (e instanceof BeforeActivateRoutes) { this.currentUrlTree = currentTransition.finalUrl!; - this.rawUrlTree = - this.urlHandlingStrategy.merge(currentTransition.finalUrl!, currentTransition.initialUrl); + this.rawUrlTree = this.urlHandlingStrategy.merge( + currentTransition.finalUrl!, + currentTransition.initialUrl, + ); this.routerState = currentTransition.targetRouterState!; if (this.urlUpdateStrategy === 'deferred') { if (!currentTransition.extras.skipLocationChange) { @@ -177,9 +194,10 @@ export class HistoryStateManager extends StateManager { } } } else if ( - e instanceof NavigationCancel && - (e.code === NavigationCancellationCode.GuardRejected || - e.code === NavigationCancellationCode.NoDataFromResolver)) { + e instanceof NavigationCancel && + (e.code === NavigationCancellationCode.GuardRejected || + e.code === NavigationCancellationCode.NoDataFromResolver) + ) { this.restoreHistory(currentTransition); } else if (e instanceof NavigationError) { this.restoreHistory(currentTransition, true); @@ -196,13 +214,13 @@ export class HistoryStateManager extends StateManager { const currentBrowserPageId = this.browserPageId; const state = { ...transition.extras.state, - ...this.generateNgRouterState(transition.id, currentBrowserPageId) + ...this.generateNgRouterState(transition.id, currentBrowserPageId), }; this.location.replaceState(path, '', state); } else { const state = { ...transition.extras.state, - ...this.generateNgRouterState(transition.id, this.browserPageId + 1) + ...this.generateNgRouterState(transition.id, this.browserPageId + 1), }; this.location.go(path, '', state); } @@ -248,14 +266,18 @@ export class HistoryStateManager extends StateManager { // the part of the navigation handled by the Angular router rather than the whole URL. In // addition, the URLHandlingStrategy may be configured to specifically preserve parts of the URL // when merging, such as the query params so they are not lost on a refresh. - this.rawUrlTree = - this.urlHandlingStrategy.merge(this.currentUrlTree, navigation.finalUrl ?? this.rawUrlTree); + this.rawUrlTree = this.urlHandlingStrategy.merge( + this.currentUrlTree, + navigation.finalUrl ?? this.rawUrlTree, + ); } private resetUrlToCurrentUrlTree(): void { this.location.replaceState( - this.urlSerializer.serialize(this.rawUrlTree), '', - this.generateNgRouterState(this.lastSuccessfulId, this.currentPageId)); + this.urlSerializer.serialize(this.rawUrlTree), + '', + this.generateNgRouterState(this.lastSuccessfulId, this.currentPageId), + ); } private generateNgRouterState(navigationId: number, routerPageId: number) { diff --git a/packages/router/src/url_tree.ts b/packages/router/src/url_tree.ts index da4f2f443ca..05f1d0d274f 100644 --- a/packages/router/src/url_tree.ts +++ b/packages/router/src/url_tree.ts @@ -12,7 +12,6 @@ import {RuntimeErrorCode} from './errors'; import {convertToParamMap, ParamMap, Params, PRIMARY_OUTLET} from './shared'; import {equalArraysOrString, shallowEqual} from './utils/collection'; - /** * A set of options which specify how to determine if a `UrlTree` is active, given the `UrlTree` * for the current router state. @@ -34,7 +33,7 @@ export interface IsActiveMatchOptions { * extra matrix parameters, but those that exist in the `UrlTree` in question must match. * - `'ignored'`: When comparing `UrlTree`s, matrix params will be ignored. */ - matrixParams: 'exact'|'subset'|'ignored'; + matrixParams: 'exact' | 'subset' | 'ignored'; /** * Defines the strategy for comparing the query parameters of two `UrlTree`s. * @@ -43,7 +42,7 @@ export interface IsActiveMatchOptions { * but must match the key and value of any that exist in the `UrlTree` in question. * - `'ignored'`: When comparing `UrlTree`s, query params will be ignored. */ - queryParams: 'exact'|'subset'|'ignored'; + queryParams: 'exact' | 'subset' | 'ignored'; /** * Defines the strategy for comparing the `UrlSegment`s of the `UrlTree`s. * @@ -52,20 +51,22 @@ export interface IsActiveMatchOptions { * is a subtree of the active route. That is, the active route may contain extra * segments, but must at least have all the segments of the `UrlTree` in question. */ - paths: 'exact'|'subset'; + paths: 'exact' | 'subset'; /** * - `'exact'`: indicates that the `UrlTree` fragments must be equal. * - `'ignored'`: the fragments will not be compared when determining if a * `UrlTree` is active. */ - fragment: 'exact'|'ignored'; + fragment: 'exact' | 'ignored'; } -type ParamMatchOptions = 'exact'|'subset'|'ignored'; +type ParamMatchOptions = 'exact' | 'subset' | 'ignored'; -type PathCompareFn = - (container: UrlSegmentGroup, containee: UrlSegmentGroup, matrixParams: ParamMatchOptions) => - boolean; +type PathCompareFn = ( + container: UrlSegmentGroup, + containee: UrlSegmentGroup, + matrixParams: ParamMatchOptions, +) => boolean; type ParamCompareFn = (container: Params, containee: Params) => boolean; const pathCompareMap: Record = { @@ -79,10 +80,15 @@ const paramCompareMap: Record = { }; export function containsTree( - container: UrlTree, containee: UrlTree, options: IsActiveMatchOptions): boolean { - return pathCompareMap[options.paths](container.root, containee.root, options.matrixParams) && - paramCompareMap[options.queryParams](container.queryParams, containee.queryParams) && - !(options.fragment === 'exact' && container.fragment !== containee.fragment); + container: UrlTree, + containee: UrlTree, + options: IsActiveMatchOptions, +): boolean { + return ( + pathCompareMap[options.paths](container.root, containee.root, options.matrixParams) && + paramCompareMap[options.queryParams](container.queryParams, containee.queryParams) && + !(options.fragment === 'exact' && container.fragment !== containee.fragment) + ); } function equalParams(container: Params, containee: Params): boolean { @@ -91,8 +97,10 @@ function equalParams(container: Params, containee: Params): boolean { } function equalSegmentGroups( - container: UrlSegmentGroup, containee: UrlSegmentGroup, - matrixParams: ParamMatchOptions): boolean { + container: UrlSegmentGroup, + containee: UrlSegmentGroup, + matrixParams: ParamMatchOptions, +): boolean { if (!equalPath(container.segments, containee.segments)) return false; if (!matrixParamsMatch(container.segments, containee.segments, matrixParams)) { return false; @@ -107,26 +115,32 @@ function equalSegmentGroups( } function containsParams(container: Params, containee: Params): boolean { - return Object.keys(containee).length <= Object.keys(container).length && - Object.keys(containee).every(key => equalArraysOrString(container[key], containee[key])); + return ( + Object.keys(containee).length <= Object.keys(container).length && + Object.keys(containee).every((key) => equalArraysOrString(container[key], containee[key])) + ); } function containsSegmentGroup( - container: UrlSegmentGroup, containee: UrlSegmentGroup, - matrixParams: ParamMatchOptions): boolean { + container: UrlSegmentGroup, + containee: UrlSegmentGroup, + matrixParams: ParamMatchOptions, +): boolean { return containsSegmentGroupHelper(container, containee, containee.segments, matrixParams); } function containsSegmentGroupHelper( - container: UrlSegmentGroup, containee: UrlSegmentGroup, containeePaths: UrlSegment[], - matrixParams: ParamMatchOptions): boolean { + container: UrlSegmentGroup, + containee: UrlSegmentGroup, + containeePaths: UrlSegment[], + matrixParams: ParamMatchOptions, +): boolean { if (container.segments.length > containeePaths.length) { const current = container.segments.slice(0, containeePaths.length); if (!equalPath(current, containeePaths)) return false; if (containee.hasChildren()) return false; if (!matrixParamsMatch(current, containeePaths, matrixParams)) return false; return true; - } else if (container.segments.length === containeePaths.length) { if (!equalPath(container.segments, containeePaths)) return false; if (!matrixParamsMatch(container.segments, containeePaths, matrixParams)) return false; @@ -137,7 +151,6 @@ function containsSegmentGroupHelper( } } return true; - } else { const current = containeePaths.slice(0, container.segments.length); const next = containeePaths.slice(container.segments.length); @@ -145,12 +158,19 @@ function containsSegmentGroupHelper( if (!matrixParamsMatch(container.segments, current, matrixParams)) return false; if (!container.children[PRIMARY_OUTLET]) return false; return containsSegmentGroupHelper( - container.children[PRIMARY_OUTLET], containee, next, matrixParams); + container.children[PRIMARY_OUTLET], + containee, + next, + matrixParams, + ); } } function matrixParamsMatch( - containerPaths: UrlSegment[], containeePaths: UrlSegment[], options: ParamMatchOptions) { + containerPaths: UrlSegment[], + containeePaths: UrlSegment[], + options: ParamMatchOptions, +) { return containeePaths.every((containeeSegment, i) => { return paramCompareMap[options](containerPaths[i].parameters, containeeSegment.parameters); }); @@ -191,18 +211,20 @@ export class UrlTree { _queryParamMap?: ParamMap; constructor( - /** The root segment group of the URL tree */ - public root: UrlSegmentGroup = new UrlSegmentGroup([], {}), - /** The query params of the URL */ - public queryParams: Params = {}, - /** The fragment of the URL */ - public fragment: string|null = null) { + /** The root segment group of the URL tree */ + public root: UrlSegmentGroup = new UrlSegmentGroup([], {}), + /** The query params of the URL */ + public queryParams: Params = {}, + /** The fragment of the URL */ + public fragment: string | null = null, + ) { if (typeof ngDevMode === 'undefined' || ngDevMode) { if (root.segments.length > 0) { throw new RuntimeError( - RuntimeErrorCode.INVALID_ROOT_URL_SEGMENT, - 'The root `UrlSegmentGroup` should not contain `segments`. ' + - 'Instead, these segments belong in the `children` so they can be associated with a named outlet.'); + RuntimeErrorCode.INVALID_ROOT_URL_SEGMENT, + 'The root `UrlSegmentGroup` should not contain `segments`. ' + + 'Instead, these segments belong in the `children` so they can be associated with a named outlet.', + ); } } } @@ -229,13 +251,14 @@ export class UrlTree { */ export class UrlSegmentGroup { /** The parent node in the url tree */ - parent: UrlSegmentGroup|null = null; + parent: UrlSegmentGroup | null = null; constructor( - /** The URL segments of this group. See `UrlSegment` for more information */ - public segments: UrlSegment[], - /** The list of children of this group */ - public children: {[key: string]: UrlSegmentGroup}) { + /** The URL segments of this group. See `UrlSegment` for more information */ + public segments: UrlSegment[], + /** The list of children of this group */ + public children: {[key: string]: UrlSegmentGroup}, + ) { Object.values(children).forEach((v) => (v.parent = this)); } @@ -255,7 +278,6 @@ export class UrlSegmentGroup { } } - /** * @description * @@ -287,11 +309,12 @@ export class UrlSegment { _parameterMap?: ParamMap; constructor( - /** The path part of a URL segment */ - public path: string, + /** The path part of a URL segment */ + public path: string, - /** The matrix parameters associated with a segment */ - public parameters: {[name: string]: string}) {} + /** The matrix parameters associated with a segment */ + public parameters: {[name: string]: string}, + ) {} get parameterMap(): ParamMap { this._parameterMap ??= convertToParamMap(this.parameters); @@ -314,7 +337,9 @@ export function equalPath(as: UrlSegment[], bs: UrlSegment[]): boolean { } export function mapChildrenIntoArray( - segment: UrlSegmentGroup, fn: (v: UrlSegmentGroup, k: string) => T[]): T[] { + segment: UrlSegmentGroup, + fn: (v: UrlSegmentGroup, k: string) => T[], +): T[] { let res: T[] = []; Object.entries(segment.children).forEach(([childOutlet, child]) => { if (childOutlet === PRIMARY_OUTLET) { @@ -329,7 +354,6 @@ export function mapChildrenIntoArray( return res; } - /** * @description * @@ -381,7 +405,7 @@ export class DefaultUrlSerializer implements UrlSerializer { const segment = `/${serializeSegment(tree.root, true)}`; const query = serializeQueryParams(tree.queryParams); const fragment = - typeof tree.fragment === `string` ? `#${encodeUriFragment(tree.fragment)}` : ''; + typeof tree.fragment === `string` ? `#${encodeUriFragment(tree.fragment)}` : ''; return `${segment}${query}${fragment}`; } @@ -390,7 +414,7 @@ export class DefaultUrlSerializer implements UrlSerializer { const DEFAULT_SERIALIZER = new DefaultUrlSerializer(); export function serializePaths(segment: UrlSegmentGroup): string { - return segment.segments.map(p => serializePath(p)).join('/'); + return segment.segments.map((p) => serializePath(p)).join('/'); } function serializeSegment(segment: UrlSegmentGroup, root: boolean): string { @@ -399,9 +423,9 @@ function serializeSegment(segment: UrlSegmentGroup, root: boolean): string { } if (root) { - const primary = segment.children[PRIMARY_OUTLET] ? - serializeSegment(segment.children[PRIMARY_OUTLET], false) : - ''; + const primary = segment.children[PRIMARY_OUTLET] + ? serializeSegment(segment.children[PRIMARY_OUTLET], false) + : ''; const children: string[] = []; Object.entries(segment.children).forEach(([k, v]) => { @@ -411,7 +435,6 @@ function serializeSegment(segment: UrlSegmentGroup, root: boolean): string { }); return children.length > 0 ? `${primary}(${children.join('//')})` : primary; - } else { const children = mapChildrenIntoArray(segment, (v: UrlSegmentGroup, k: string) => { if (k === PRIMARY_OUTLET) { @@ -438,10 +461,10 @@ function serializeSegment(segment: UrlSegmentGroup, root: boolean): string { */ function encodeUriString(s: string): string { return encodeURIComponent(s) - .replace(/%40/g, '@') - .replace(/%3A/gi, ':') - .replace(/%24/g, '$') - .replace(/%2C/gi, ','); + .replace(/%40/g, '@') + .replace(/%3A/gi, ':') + .replace(/%24/g, '$') + .replace(/%2C/gi, ','); } /** @@ -491,19 +514,18 @@ export function serializePath(path: UrlSegment): string { function serializeMatrixParams(params: {[key: string]: string}): string { return Object.entries(params) - .map(([key, value]) => `;${encodeUriSegment(key)}=${encodeUriSegment(value)}`) - .join(''); + .map(([key, value]) => `;${encodeUriSegment(key)}=${encodeUriSegment(value)}`) + .join(''); } function serializeQueryParams(params: {[key: string]: any}): string { - const strParams: string[] = - Object.entries(params) - .map(([name, value]) => { - return Array.isArray(value) ? - value.map(v => `${encodeUriQuery(name)}=${encodeUriQuery(v)}`).join('&') : - `${encodeUriQuery(name)}=${encodeUriQuery(value)}`; - }) - .filter(s => s); + const strParams: string[] = Object.entries(params) + .map(([name, value]) => { + return Array.isArray(value) + ? value.map((v) => `${encodeUriQuery(name)}=${encodeUriQuery(v)}`).join('&') + : `${encodeUriQuery(name)}=${encodeUriQuery(value)}`; + }) + .filter((s) => s); return strParams.length ? `?${strParams.join('&')}` : ''; } @@ -562,7 +584,7 @@ class UrlParser { return params; } - parseFragment(): string|null { + parseFragment(): string | null { return this.consumeOptional('#') ? decodeURIComponent(this.remaining) : null; } @@ -607,9 +629,10 @@ class UrlParser { const path = matchSegments(this.remaining); if (path === '' && this.peekStartsWith(';')) { throw new RuntimeError( - RuntimeErrorCode.EMPTY_PATH_WITH_PARAMS, - (typeof ngDevMode === 'undefined' || ngDevMode) && - `Empty path url segment cannot have parameters: '${this.remaining}'.`); + RuntimeErrorCode.EMPTY_PATH_WITH_PARAMS, + (typeof ngDevMode === 'undefined' || ngDevMode) && + `Empty path url segment cannot have parameters: '${this.remaining}'.`, + ); } this.capture(path); @@ -689,8 +712,9 @@ class UrlParser { // or the group was not closed if (next !== '/' && next !== ')' && next !== ';') { throw new RuntimeError( - RuntimeErrorCode.UNPARSABLE_URL, - (typeof ngDevMode === 'undefined' || ngDevMode) && `Cannot parse url '${this.url}'`); + RuntimeErrorCode.UNPARSABLE_URL, + (typeof ngDevMode === 'undefined' || ngDevMode) && `Cannot parse url '${this.url}'`, + ); } let outletName: string = undefined!; @@ -703,8 +727,10 @@ class UrlParser { } const children = this.parseChildren(); - segments[outletName] = Object.keys(children).length === 1 ? children[PRIMARY_OUTLET] : - new UrlSegmentGroup([], children); + segments[outletName] = + Object.keys(children).length === 1 + ? children[PRIMARY_OUTLET] + : new UrlSegmentGroup([], children); this.consumeOptional('//'); } @@ -727,16 +753,17 @@ class UrlParser { private capture(str: string): void { if (!this.consumeOptional(str)) { throw new RuntimeError( - RuntimeErrorCode.UNEXPECTED_VALUE_IN_URL, - (typeof ngDevMode === 'undefined' || ngDevMode) && `Expected "${str}".`); + RuntimeErrorCode.UNEXPECTED_VALUE_IN_URL, + (typeof ngDevMode === 'undefined' || ngDevMode) && `Expected "${str}".`, + ); } } } export function createRoot(rootCandidate: UrlSegmentGroup) { - return rootCandidate.segments.length > 0 ? - new UrlSegmentGroup([], {[PRIMARY_OUTLET]: rootCandidate}) : - rootCandidate; + return rootCandidate.segments.length > 0 + ? new UrlSegmentGroup([], {[PRIMARY_OUTLET]: rootCandidate}) + : rootCandidate; } /** @@ -754,12 +781,15 @@ export function squashSegmentGroup(segmentGroup: UrlSegmentGroup): UrlSegmentGro for (const [childOutlet, child] of Object.entries(segmentGroup.children)) { const childCandidate = squashSegmentGroup(child); // moves named children in an empty path primary child into this group - if (childOutlet === PRIMARY_OUTLET && childCandidate.segments.length === 0 && - childCandidate.hasChildren()) { + if ( + childOutlet === PRIMARY_OUTLET && + childCandidate.segments.length === 0 && + childCandidate.hasChildren() + ) { for (const [grandChildOutlet, grandChild] of Object.entries(childCandidate.children)) { newChildren[grandChildOutlet] = grandChild; } - } // don't add empty children + } // don't add empty children else if (childCandidate.segments.length > 0 || childCandidate.hasChildren()) { newChildren[childOutlet] = childCandidate; } diff --git a/packages/router/src/utils/collection.ts b/packages/router/src/utils/collection.ts index b6cd8ec59aa..96d2ecc38b7 100644 --- a/packages/router/src/utils/collection.ts +++ b/packages/router/src/utils/collection.ts @@ -18,7 +18,9 @@ export function shallowEqualArrays(a: any[], b: any[]): boolean { } export function shallowEqual( - a: {[key: string|symbol]: any}, b: {[key: string|symbol]: any}): boolean { + a: {[key: string | symbol]: any}, + b: {[key: string | symbol]: any}, +): boolean { // While `undefined` should never be possible, it would sometimes be the case in IE 11 // and pre-chromium Edge. The check below accounts for this edge case. const k1 = a ? getDataKeys(a) : undefined; @@ -26,7 +28,7 @@ export function shallowEqual( if (!k1 || !k2 || k1.length != k2.length) { return false; } - let key: string|symbol; + let key: string | symbol; for (let i = 0; i < k1.length; i++) { key = k1[i]; if (!equalArraysOrString(a[key], b[key])) { @@ -39,14 +41,14 @@ export function shallowEqual( /** * Gets the keys of an object, including `symbol` keys. */ -export function getDataKeys(obj: Object): Array { +export function getDataKeys(obj: Object): Array { return [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)]; } /** * Test equality for arrays of strings or a string. */ -export function equalArraysOrString(a: string|string[], b: string|string[]) { +export function equalArraysOrString(a: string | string[], b: string | string[]) { if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; const aSorted = [...a].sort(); @@ -60,11 +62,11 @@ export function equalArraysOrString(a: string|string[], b: string|string[]) { /** * Return the last element of an array. */ -export function last(a: T[]): T|null { +export function last(a: T[]): T | null { return a.length > 0 ? a[a.length - 1] : null; } -export function wrapIntoObservable(value: T|Promise|Observable): Observable { +export function wrapIntoObservable(value: T | Promise | Observable): Observable { if (isObservable(value)) { return value; } diff --git a/packages/router/src/utils/config.ts b/packages/router/src/utils/config.ts index 38d4835b28b..80972c02735 100644 --- a/packages/router/src/utils/config.ts +++ b/packages/router/src/utils/config.ts @@ -6,7 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {createEnvironmentInjector, EnvironmentInjector, isStandalone, Type, ɵisNgModule as isNgModule, ɵRuntimeError as RuntimeError} from '@angular/core'; +import { + createEnvironmentInjector, + EnvironmentInjector, + isStandalone, + Type, + ɵisNgModule as isNgModule, + ɵRuntimeError as RuntimeError, +} from '@angular/core'; import {EmptyOutletComponent} from '../components/empty_outlet'; import {RuntimeErrorCode} from '../errors'; @@ -23,31 +30,39 @@ import {PRIMARY_OUTLET} from '../shared'; * @param currentInjector The parent injector of the `Route` */ export function getOrCreateRouteInjectorIfNeeded( - route: Route, currentInjector: EnvironmentInjector) { + route: Route, + currentInjector: EnvironmentInjector, +) { if (route.providers && !route._injector) { - route._injector = - createEnvironmentInjector(route.providers, currentInjector, `Route: ${route.path}`); + route._injector = createEnvironmentInjector( + route.providers, + currentInjector, + `Route: ${route.path}`, + ); } return route._injector ?? currentInjector; } -export function getLoadedRoutes(route: Route): Route[]|undefined { +export function getLoadedRoutes(route: Route): Route[] | undefined { return route._loadedRoutes; } -export function getLoadedInjector(route: Route): EnvironmentInjector|undefined { +export function getLoadedInjector(route: Route): EnvironmentInjector | undefined { return route._loadedInjector; } -export function getLoadedComponent(route: Route): Type|undefined { +export function getLoadedComponent(route: Route): Type | undefined { return route._loadedComponent; } -export function getProvidersInjector(route: Route): EnvironmentInjector|undefined { +export function getProvidersInjector(route: Route): EnvironmentInjector | undefined { return route._injector; } export function validateConfig( - config: Routes, parentPath: string = '', requireStandaloneComponents = false): void { + config: Routes, + parentPath: string = '', + requireStandaloneComponents = false, +): void { // forEach doesn't iterate undefined values for (let i = 0; i < config.length; i++) { const route: Route = config[i]; @@ -56,24 +71,27 @@ export function validateConfig( } } -export function assertStandalone(fullPath: string, component: Type|undefined) { +export function assertStandalone(fullPath: string, component: Type | undefined) { if (component && isNgModule(component)) { throw new RuntimeError( - RuntimeErrorCode.INVALID_ROUTE_CONFIG, - `Invalid configuration of route '${ - fullPath}'. You are using 'loadComponent' with a module, ` + - `but it must be used with standalone components. Use 'loadChildren' instead.`); + RuntimeErrorCode.INVALID_ROUTE_CONFIG, + `Invalid configuration of route '${fullPath}'. You are using 'loadComponent' with a module, ` + + `but it must be used with standalone components. Use 'loadChildren' instead.`, + ); } else if (component && !isStandalone(component)) { throw new RuntimeError( - RuntimeErrorCode.INVALID_ROUTE_CONFIG, - `Invalid configuration of route '${fullPath}'. The component must be standalone.`); + RuntimeErrorCode.INVALID_ROUTE_CONFIG, + `Invalid configuration of route '${fullPath}'. The component must be standalone.`, + ); } } function validateNode(route: Route, fullPath: string, requireStandaloneComponents: boolean): void { if (typeof ngDevMode === 'undefined' || ngDevMode) { if (!route) { - throw new RuntimeError(RuntimeErrorCode.INVALID_ROUTE_CONFIG, ` + throw new RuntimeError( + RuntimeErrorCode.INVALID_ROUTE_CONFIG, + ` Invalid configuration of route '${fullPath}': Encountered undefined route. The reason might be an extra comma. @@ -83,87 +101,102 @@ function validateNode(route: Route, fullPath: string, requireStandaloneComponent { path: 'dashboard', component: DashboardComponent },, << two commas { path: 'detail/:id', component: HeroDetailComponent } ]; - `); + `, + ); } if (Array.isArray(route)) { throw new RuntimeError( - RuntimeErrorCode.INVALID_ROUTE_CONFIG, - `Invalid configuration of route '${fullPath}': Array cannot be specified`); + RuntimeErrorCode.INVALID_ROUTE_CONFIG, + `Invalid configuration of route '${fullPath}': Array cannot be specified`, + ); } - if (!route.redirectTo && !route.component && !route.loadComponent && !route.children && - !route.loadChildren && (route.outlet && route.outlet !== PRIMARY_OUTLET)) { + if ( + !route.redirectTo && + !route.component && + !route.loadComponent && + !route.children && + !route.loadChildren && + route.outlet && + route.outlet !== PRIMARY_OUTLET + ) { throw new RuntimeError( - RuntimeErrorCode.INVALID_ROUTE_CONFIG, - `Invalid configuration of route '${ - fullPath}': a componentless route without children or loadChildren cannot have a named outlet set`); + RuntimeErrorCode.INVALID_ROUTE_CONFIG, + `Invalid configuration of route '${fullPath}': a componentless route without children or loadChildren cannot have a named outlet set`, + ); } if (route.redirectTo && route.children) { throw new RuntimeError( - RuntimeErrorCode.INVALID_ROUTE_CONFIG, - `Invalid configuration of route '${ - fullPath}': redirectTo and children cannot be used together`); + RuntimeErrorCode.INVALID_ROUTE_CONFIG, + `Invalid configuration of route '${fullPath}': redirectTo and children cannot be used together`, + ); } if (route.redirectTo && route.loadChildren) { throw new RuntimeError( - RuntimeErrorCode.INVALID_ROUTE_CONFIG, - `Invalid configuration of route '${ - fullPath}': redirectTo and loadChildren cannot be used together`); + RuntimeErrorCode.INVALID_ROUTE_CONFIG, + `Invalid configuration of route '${fullPath}': redirectTo and loadChildren cannot be used together`, + ); } if (route.children && route.loadChildren) { throw new RuntimeError( - RuntimeErrorCode.INVALID_ROUTE_CONFIG, - `Invalid configuration of route '${ - fullPath}': children and loadChildren cannot be used together`); + RuntimeErrorCode.INVALID_ROUTE_CONFIG, + `Invalid configuration of route '${fullPath}': children and loadChildren cannot be used together`, + ); } if (route.redirectTo && (route.component || route.loadComponent)) { throw new RuntimeError( - RuntimeErrorCode.INVALID_ROUTE_CONFIG, - `Invalid configuration of route '${ - fullPath}': redirectTo and component/loadComponent cannot be used together`); + RuntimeErrorCode.INVALID_ROUTE_CONFIG, + `Invalid configuration of route '${fullPath}': redirectTo and component/loadComponent cannot be used together`, + ); } if (route.component && route.loadComponent) { throw new RuntimeError( - RuntimeErrorCode.INVALID_ROUTE_CONFIG, - `Invalid configuration of route '${ - fullPath}': component and loadComponent cannot be used together`); + RuntimeErrorCode.INVALID_ROUTE_CONFIG, + `Invalid configuration of route '${fullPath}': component and loadComponent cannot be used together`, + ); } if (route.redirectTo && route.canActivate) { throw new RuntimeError( - RuntimeErrorCode.INVALID_ROUTE_CONFIG, - `Invalid configuration of route '${ - fullPath}': redirectTo and canActivate cannot be used together. Redirects happen before activation ` + - `so canActivate will never be executed.`); + RuntimeErrorCode.INVALID_ROUTE_CONFIG, + `Invalid configuration of route '${fullPath}': redirectTo and canActivate cannot be used together. Redirects happen before activation ` + + `so canActivate will never be executed.`, + ); } if (route.path && route.matcher) { throw new RuntimeError( - RuntimeErrorCode.INVALID_ROUTE_CONFIG, - `Invalid configuration of route '${fullPath}': path and matcher cannot be used together`); + RuntimeErrorCode.INVALID_ROUTE_CONFIG, + `Invalid configuration of route '${fullPath}': path and matcher cannot be used together`, + ); } - if (route.redirectTo === void 0 && !route.component && !route.loadComponent && - !route.children && !route.loadChildren) { + if ( + route.redirectTo === void 0 && + !route.component && + !route.loadComponent && + !route.children && + !route.loadChildren + ) { throw new RuntimeError( - RuntimeErrorCode.INVALID_ROUTE_CONFIG, - `Invalid configuration of route '${ - fullPath}'. One of the following must be provided: component, loadComponent, redirectTo, children or loadChildren`); + RuntimeErrorCode.INVALID_ROUTE_CONFIG, + `Invalid configuration of route '${fullPath}'. One of the following must be provided: component, loadComponent, redirectTo, children or loadChildren`, + ); } if (route.path === void 0 && route.matcher === void 0) { throw new RuntimeError( - RuntimeErrorCode.INVALID_ROUTE_CONFIG, - `Invalid configuration of route '${ - fullPath}': routes must have either a path or a matcher specified`); + RuntimeErrorCode.INVALID_ROUTE_CONFIG, + `Invalid configuration of route '${fullPath}': routes must have either a path or a matcher specified`, + ); } if (typeof route.path === 'string' && route.path.charAt(0) === '/') { throw new RuntimeError( - RuntimeErrorCode.INVALID_ROUTE_CONFIG, - `Invalid configuration of route '${fullPath}': path cannot start with a slash`); + RuntimeErrorCode.INVALID_ROUTE_CONFIG, + `Invalid configuration of route '${fullPath}': path cannot start with a slash`, + ); } if (route.path === '' && route.redirectTo !== void 0 && route.pathMatch === void 0) { - const exp = - `The default value of 'pathMatch' is 'prefix', but often the intent is to use 'full'.`; + const exp = `The default value of 'pathMatch' is 'prefix', but often the intent is to use 'full'.`; throw new RuntimeError( - RuntimeErrorCode.INVALID_ROUTE_CONFIG, - `Invalid configuration of route '{path: "${fullPath}", redirectTo: "${ - route.redirectTo}"}': please provide 'pathMatch'. ${exp}`); + RuntimeErrorCode.INVALID_ROUTE_CONFIG, + `Invalid configuration of route '{path: "${fullPath}", redirectTo: "${route.redirectTo}"}': please provide 'pathMatch'. ${exp}`, + ); } if (requireStandaloneComponents) { assertStandalone(fullPath, route.component); @@ -195,8 +228,13 @@ function getFullPath(parentPath: string, currentRoute: Route): string { export function standardizeConfig(r: Route): Route { const children = r.children && r.children.map(standardizeConfig); const c = children ? {...r, children} : {...r}; - if ((!c.component && !c.loadComponent) && (children || c.loadChildren) && - (c.outlet && c.outlet !== PRIMARY_OUTLET)) { + if ( + !c.component && + !c.loadComponent && + (children || c.loadChildren) && + c.outlet && + c.outlet !== PRIMARY_OUTLET + ) { c.component = EmptyOutletComponent; } return c; @@ -212,8 +250,8 @@ export function getOutlet(route: Route): string { * The order of the configs is otherwise preserved. */ export function sortByMatchingOutlets(routes: Routes, outletName: string): Routes { - const sortedConfig = routes.filter(r => getOutlet(r) === outletName); - sortedConfig.push(...routes.filter(r => getOutlet(r) !== outletName)); + const sortedConfig = routes.filter((r) => getOutlet(r) === outletName); + sortedConfig.push(...routes.filter((r) => getOutlet(r) !== outletName)); return sortedConfig; } @@ -229,8 +267,9 @@ export function sortByMatchingOutlets(routes: Routes, outletName: string): Route * Generally used for retrieving the injector to use for getting tokens for guards/resolvers and * also used for getting the correct injector to use for creating components. */ -export function getClosestRouteInjector(snapshot: ActivatedRouteSnapshot): EnvironmentInjector| - null { +export function getClosestRouteInjector( + snapshot: ActivatedRouteSnapshot, +): EnvironmentInjector | null { if (!snapshot) return null; // If the current route has its own injector, which is created from the static providers on the diff --git a/packages/router/src/utils/config_matching.ts b/packages/router/src/utils/config_matching.ts index 14e81364084..e3353853c6a 100644 --- a/packages/router/src/utils/config_matching.ts +++ b/packages/router/src/utils/config_matching.ts @@ -31,12 +31,16 @@ const noMatch: MatchResult = { consumedSegments: [], remainingSegments: [], parameters: {}, - positionalParamSegments: {} + positionalParamSegments: {}, }; export function matchWithChecks( - segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[], - injector: EnvironmentInjector, urlSerializer: UrlSerializer): Observable { + segmentGroup: UrlSegmentGroup, + route: Route, + segments: UrlSegment[], + injector: EnvironmentInjector, + urlSerializer: UrlSerializer, +): Observable { const result = match(segmentGroup, route, segments); if (!result.matched) { return of(result); @@ -45,14 +49,16 @@ export function matchWithChecks( // Only create the Route's `EnvironmentInjector` if it matches the attempted // navigation injector = getOrCreateRouteInjectorIfNeeded(route, injector); - return runCanMatchGuards(injector, route, segments, urlSerializer) - .pipe( - map((v) => v === true ? result : {...noMatch}), - ); + return runCanMatchGuards(injector, route, segments, urlSerializer).pipe( + map((v) => (v === true ? result : {...noMatch})), + ); } export function match( - segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[]): MatchResult { + segmentGroup: UrlSegmentGroup, + route: Route, + segments: UrlSegment[], +): MatchResult { if (route.path === '**') { return createWildcardMatchResult(segments); } @@ -67,7 +73,7 @@ export function match( consumedSegments: [], remainingSegments: segments, parameters: {}, - positionalParamSegments: {} + positionalParamSegments: {}, }; } @@ -79,9 +85,10 @@ export function match( Object.entries(res.posParams ?? {}).forEach(([k, v]) => { posParams[k] = v.path; }); - const parameters = res.consumed.length > 0 ? - {...posParams, ...res.consumed[res.consumed.length - 1].parameters} : - posParams; + const parameters = + res.consumed.length > 0 + ? {...posParams, ...res.consumed[res.consumed.length - 1].parameters} + : posParams; return { matched: true, @@ -89,7 +96,7 @@ export function match( remainingSegments: segments.slice(res.consumed.length), // TODO(atscott): investigate combining parameters and positionalParamSegments parameters, - positionalParamSegments: res.posParams ?? {} + positionalParamSegments: res.posParams ?? {}, }; } @@ -104,23 +111,33 @@ function createWildcardMatchResult(segments: UrlSegment[]): MatchResult { } export function split( - segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[], - config: Route[]) { - if (slicedSegments.length > 0 && - containsEmptyPathMatchesWithNamedOutlets(segmentGroup, slicedSegments, config)) { + segmentGroup: UrlSegmentGroup, + consumedSegments: UrlSegment[], + slicedSegments: UrlSegment[], + config: Route[], +) { + if ( + slicedSegments.length > 0 && + containsEmptyPathMatchesWithNamedOutlets(segmentGroup, slicedSegments, config) + ) { const s = new UrlSegmentGroup( - consumedSegments, - createChildrenForEmptyPaths( - config, new UrlSegmentGroup(slicedSegments, segmentGroup.children))); + consumedSegments, + createChildrenForEmptyPaths( + config, + new UrlSegmentGroup(slicedSegments, segmentGroup.children), + ), + ); return {segmentGroup: s, slicedSegments: []}; } - if (slicedSegments.length === 0 && - containsEmptyPathMatches(segmentGroup, slicedSegments, config)) { + if ( + slicedSegments.length === 0 && + containsEmptyPathMatches(segmentGroup, slicedSegments, config) + ) { const s = new UrlSegmentGroup( - segmentGroup.segments, - addEmptyPathsToChildrenIfNeeded( - segmentGroup, slicedSegments, config, segmentGroup.children)); + segmentGroup.segments, + addEmptyPathsToChildrenIfNeeded(segmentGroup, slicedSegments, config, segmentGroup.children), + ); return {segmentGroup: s, slicedSegments}; } @@ -129,8 +146,11 @@ export function split( } function addEmptyPathsToChildrenIfNeeded( - segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[], - children: {[name: string]: UrlSegmentGroup}): {[name: string]: UrlSegmentGroup} { + segmentGroup: UrlSegmentGroup, + slicedSegments: UrlSegment[], + routes: Route[], + children: {[name: string]: UrlSegmentGroup}, +): {[name: string]: UrlSegmentGroup} { const res: {[name: string]: UrlSegmentGroup} = {}; for (const r of routes) { if (emptyPathMatch(segmentGroup, slicedSegments, r) && !children[getOutlet(r)]) { @@ -142,7 +162,9 @@ function addEmptyPathsToChildrenIfNeeded( } function createChildrenForEmptyPaths( - routes: Route[], primarySegment: UrlSegmentGroup): {[name: string]: UrlSegmentGroup} { + routes: Route[], + primarySegment: UrlSegmentGroup, +): {[name: string]: UrlSegmentGroup} { const res: {[name: string]: UrlSegmentGroup} = {}; res[PRIMARY_OUTLET] = primarySegment; @@ -156,18 +178,28 @@ function createChildrenForEmptyPaths( } function containsEmptyPathMatchesWithNamedOutlets( - segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean { + segmentGroup: UrlSegmentGroup, + slicedSegments: UrlSegment[], + routes: Route[], +): boolean { return routes.some( - r => emptyPathMatch(segmentGroup, slicedSegments, r) && getOutlet(r) !== PRIMARY_OUTLET); + (r) => emptyPathMatch(segmentGroup, slicedSegments, r) && getOutlet(r) !== PRIMARY_OUTLET, + ); } function containsEmptyPathMatches( - segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean { - return routes.some(r => emptyPathMatch(segmentGroup, slicedSegments, r)); + segmentGroup: UrlSegmentGroup, + slicedSegments: UrlSegment[], + routes: Route[], +): boolean { + return routes.some((r) => emptyPathMatch(segmentGroup, slicedSegments, r)); } function emptyPathMatch( - segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], r: Route): boolean { + segmentGroup: UrlSegmentGroup, + slicedSegments: UrlSegment[], + r: Route, +): boolean { if ((segmentGroup.hasChildren() || slicedSegments.length > 0) && r.pathMatch === 'full') { return false; } @@ -181,7 +213,11 @@ function emptyPathMatch( * well. */ export function isImmediateMatch( - route: Route, rawSegment: UrlSegmentGroup, segments: UrlSegment[], outlet: string): boolean { + route: Route, + rawSegment: UrlSegmentGroup, + segments: UrlSegment[], + outlet: string, +): boolean { // We allow matches to empty paths when the outlets differ so we can match a url like `/(b:b)` to // a config like // * `{path: '', children: [{path: 'b', outlet: 'b'}]}` @@ -193,14 +229,19 @@ export function isImmediateMatch( // outlets. So we need to prevent child named outlet matches in a url like `/b` in a config like // * `{path: '', outlet: 'x' children: [{path: 'b'}]}` // This should only match if the url is `/(x:b)`. - if (getOutlet(route) !== outlet && - (outlet === PRIMARY_OUTLET || !emptyPathMatch(rawSegment, segments, route))) { + if ( + getOutlet(route) !== outlet && + (outlet === PRIMARY_OUTLET || !emptyPathMatch(rawSegment, segments, route)) + ) { return false; } return match(rawSegment, route, segments).matched; } export function noLeftoversInUrl( - segmentGroup: UrlSegmentGroup, segments: UrlSegment[], outlet: string): boolean { + segmentGroup: UrlSegmentGroup, + segments: UrlSegment[], + outlet: string, +): boolean { return segments.length === 0 && !segmentGroup.children[outlet]; } diff --git a/packages/router/src/utils/functional_guards.ts b/packages/router/src/utils/functional_guards.ts index 81bf4386c44..b19de0319b7 100644 --- a/packages/router/src/utils/functional_guards.ts +++ b/packages/router/src/utils/functional_guards.ts @@ -20,7 +20,11 @@ import {CanActivateChildFn, CanActivateFn, CanDeactivateFn, CanMatchFn, ResolveF * @see {@link Route} */ export function mapToCanMatch(providers: Array>): CanMatchFn[] { - return providers.map(provider => (...params) => inject(provider).canMatch(...params)); + return providers.map( + (provider) => + (...params) => + inject(provider).canMatch(...params), + ); } /** @@ -32,9 +36,14 @@ export function mapToCanMatch(providers: Array>): C * @publicApi * @see {@link Route} */ -export function mapToCanActivate(providers: Array>): - CanActivateFn[] { - return providers.map(provider => (...params) => inject(provider).canActivate(...params)); +export function mapToCanActivate( + providers: Array>, +): CanActivateFn[] { + return providers.map( + (provider) => + (...params) => + inject(provider).canActivate(...params), + ); } /** * Maps an array of injectable classes with canActivateChild functions to an array of equivalent @@ -46,8 +55,13 @@ export function mapToCanActivate(providers: Array>): CanActivateChildFn[] { - return providers.map(provider => (...params) => inject(provider).canActivateChild(...params)); + providers: Array>, +): CanActivateChildFn[] { + return providers.map( + (provider) => + (...params) => + inject(provider).canActivateChild(...params), + ); } /** * Maps an array of injectable classes with canDeactivate functions to an array of equivalent @@ -59,8 +73,13 @@ export function mapToCanActivateChild( * @see {@link Route} */ export function mapToCanDeactivate( - providers: Array}>>): CanDeactivateFn[] { - return providers.map(provider => (...params) => inject(provider).canDeactivate(...params)); + providers: Array}>>, +): CanDeactivateFn[] { + return providers.map( + (provider) => + (...params) => + inject(provider).canDeactivate(...params), + ); } /** * Maps an injectable class with a resolve function to an equivalent `ResolveFn` diff --git a/packages/router/src/utils/navigations.ts b/packages/router/src/utils/navigations.ts index a4b5805f0ea..23c36042b76 100644 --- a/packages/router/src/utils/navigations.ts +++ b/packages/router/src/utils/navigations.ts @@ -9,7 +9,14 @@ import {Observable} from 'rxjs'; import {filter, map, take} from 'rxjs/operators'; -import {Event, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped} from '../events'; +import { + Event, + NavigationCancel, + NavigationCancellationCode, + NavigationEnd, + NavigationError, + NavigationSkipped, +} from '../events'; enum NavigationResult { COMPLETE, @@ -28,27 +35,32 @@ enum NavigationResult { */ export function afterNextNavigation(router: {events: Observable}, action: () => void) { router.events - .pipe( - filter( - (e): e is NavigationEnd|NavigationCancel|NavigationError|NavigationSkipped => - e instanceof NavigationEnd || e instanceof NavigationCancel || - e instanceof NavigationError || e instanceof NavigationSkipped), - map(e => { - if (e instanceof NavigationEnd || e instanceof NavigationSkipped) { - return NavigationResult.COMPLETE; - } - const redirecting = e instanceof NavigationCancel ? - (e.code === NavigationCancellationCode.Redirect || - e.code === NavigationCancellationCode.SupersededByNewNavigation) : - false; - return redirecting ? NavigationResult.REDIRECTING : NavigationResult.FAILED; - }), - filter( - (result): result is NavigationResult.COMPLETE|NavigationResult.FAILED => - result !== NavigationResult.REDIRECTING), - take(1), - ) - .subscribe(() => { - action(); - }); + .pipe( + filter( + (e): e is NavigationEnd | NavigationCancel | NavigationError | NavigationSkipped => + e instanceof NavigationEnd || + e instanceof NavigationCancel || + e instanceof NavigationError || + e instanceof NavigationSkipped, + ), + map((e) => { + if (e instanceof NavigationEnd || e instanceof NavigationSkipped) { + return NavigationResult.COMPLETE; + } + const redirecting = + e instanceof NavigationCancel + ? e.code === NavigationCancellationCode.Redirect || + e.code === NavigationCancellationCode.SupersededByNewNavigation + : false; + return redirecting ? NavigationResult.REDIRECTING : NavigationResult.FAILED; + }), + filter( + (result): result is NavigationResult.COMPLETE | NavigationResult.FAILED => + result !== NavigationResult.REDIRECTING, + ), + take(1), + ) + .subscribe(() => { + action(); + }); } diff --git a/packages/router/src/utils/preactivation.ts b/packages/router/src/utils/preactivation.ts index 0821a66a6cd..f32081d05c4 100644 --- a/packages/router/src/utils/preactivation.ts +++ b/packages/router/src/utils/preactivation.ts @@ -10,7 +10,11 @@ import {Injector, ProviderToken, ɵisInjectable as isInjectable} from '@angular/ import {RunGuardsAndResolvers} from '../models'; import {ChildrenOutletContexts, OutletContext} from '../router_outlet_context'; -import {ActivatedRouteSnapshot, equalParamsAndUrlSegments, RouterStateSnapshot} from '../router_state'; +import { + ActivatedRouteSnapshot, + equalParamsAndUrlSegments, + RouterStateSnapshot, +} from '../router_state'; import {equalPath} from '../url_tree'; import {shallowEqual} from '../utils/collection'; import {nodeChildrenAsMap, TreeNode} from '../utils/tree'; @@ -23,34 +27,42 @@ export class CanActivate { } export class CanDeactivate { - constructor(public component: Object|null, public route: ActivatedRouteSnapshot) {} + constructor( + public component: Object | null, + public route: ActivatedRouteSnapshot, + ) {} } export declare type Checks = { - canDeactivateChecks: CanDeactivate[], - canActivateChecks: CanActivate[], + canDeactivateChecks: CanDeactivate[]; + canActivateChecks: CanActivate[]; }; export function getAllRouteGuards( - future: RouterStateSnapshot, curr: RouterStateSnapshot, - parentContexts: ChildrenOutletContexts) { + future: RouterStateSnapshot, + curr: RouterStateSnapshot, + parentContexts: ChildrenOutletContexts, +) { const futureRoot = future._root; const currRoot = curr ? curr._root : null; return getChildRouteGuards(futureRoot, currRoot, parentContexts, [futureRoot.value]); } -export function getCanActivateChild(p: ActivatedRouteSnapshot): - {node: ActivatedRouteSnapshot, guards: any[]}|null { +export function getCanActivateChild( + p: ActivatedRouteSnapshot, +): {node: ActivatedRouteSnapshot; guards: any[]} | null { const canActivateChild = p.routeConfig ? p.routeConfig.canActivateChild : null; if (!canActivateChild || canActivateChild.length === 0) return null; return {node: p, guards: canActivateChild}; } export function getTokenOrFunctionIdentity( - tokenOrFunction: Function|ProviderToken, injector: Injector): Function|T { + tokenOrFunction: Function | ProviderToken, + injector: Injector, +): Function | T { const NOT_FOUND = Symbol(); - const result = injector.get(tokenOrFunction, NOT_FOUND); + const result = injector.get(tokenOrFunction, NOT_FOUND); if (result === NOT_FOUND) { if (typeof tokenOrFunction === 'function' && !isInjectable(tokenOrFunction)) { // We think the token is just a function so return it as-is @@ -64,43 +76,52 @@ export function getTokenOrFunctionIdentity( } function getChildRouteGuards( - futureNode: TreeNode, currNode: TreeNode|null, - contexts: ChildrenOutletContexts|null, futurePath: ActivatedRouteSnapshot[], checks: Checks = { - canDeactivateChecks: [], - canActivateChecks: [] - }): Checks { + futureNode: TreeNode, + currNode: TreeNode | null, + contexts: ChildrenOutletContexts | null, + futurePath: ActivatedRouteSnapshot[], + checks: Checks = { + canDeactivateChecks: [], + canActivateChecks: [], + }, +): Checks { const prevChildren = nodeChildrenAsMap(currNode); // Process the children of the future route - futureNode.children.forEach(c => { + futureNode.children.forEach((c) => { getRouteGuards(c, prevChildren[c.value.outlet], contexts, futurePath.concat([c.value]), checks); delete prevChildren[c.value.outlet]; }); // Process any children left from the current route (not active for the future route) - Object.entries(prevChildren) - .forEach( - ([k, v]: [string, TreeNode]) => - deactivateRouteAndItsChildren(v, contexts!.getContext(k), checks)); + Object.entries(prevChildren).forEach(([k, v]: [string, TreeNode]) => + deactivateRouteAndItsChildren(v, contexts!.getContext(k), checks), + ); return checks; } function getRouteGuards( - futureNode: TreeNode, currNode: TreeNode, - parentContexts: ChildrenOutletContexts|null, futurePath: ActivatedRouteSnapshot[], - checks: Checks = { - canDeactivateChecks: [], - canActivateChecks: [] - }): Checks { + futureNode: TreeNode, + currNode: TreeNode, + parentContexts: ChildrenOutletContexts | null, + futurePath: ActivatedRouteSnapshot[], + checks: Checks = { + canDeactivateChecks: [], + canActivateChecks: [], + }, +): Checks { const future = futureNode.value; const curr = currNode ? currNode.value : null; const context = parentContexts ? parentContexts.getContext(futureNode.value.outlet) : null; // reusing the node if (curr && future.routeConfig === curr.routeConfig) { - const shouldRun = - shouldRunGuardsAndResolvers(curr, future, future.routeConfig!.runGuardsAndResolvers); + const shouldRun = shouldRunGuardsAndResolvers( + curr, + future, + future.routeConfig!.runGuardsAndResolvers, + ); if (shouldRun) { checks.canActivateChecks.push(new CanActivate(futurePath)); } else { @@ -112,7 +133,12 @@ function getRouteGuards( // If we have a component, we need to go through an outlet. if (future.component) { getChildRouteGuards( - futureNode, currNode, context ? context.children : null, futurePath, checks); + futureNode, + currNode, + context ? context.children : null, + futurePath, + checks, + ); // if we have a componentless route, we recurse but keep the same outlet map. } else { @@ -142,8 +168,10 @@ function getRouteGuards( } function shouldRunGuardsAndResolvers( - curr: ActivatedRouteSnapshot, future: ActivatedRouteSnapshot, - mode: RunGuardsAndResolvers|undefined): boolean { + curr: ActivatedRouteSnapshot, + future: ActivatedRouteSnapshot, + mode: RunGuardsAndResolvers | undefined, +): boolean { if (typeof mode === 'function') { return mode(curr, future); } @@ -152,15 +180,18 @@ function shouldRunGuardsAndResolvers( return !equalPath(curr.url, future.url); case 'pathParamsOrQueryParamsChange': - return !equalPath(curr.url, future.url) || - !shallowEqual(curr.queryParams, future.queryParams); + return ( + !equalPath(curr.url, future.url) || !shallowEqual(curr.queryParams, future.queryParams) + ); case 'always': return true; case 'paramsOrQueryParamsChange': - return !equalParamsAndUrlSegments(curr, future) || - !shallowEqual(curr.queryParams, future.queryParams); + return ( + !equalParamsAndUrlSegments(curr, future) || + !shallowEqual(curr.queryParams, future.queryParams) + ); case 'paramsChange': default: @@ -169,7 +200,10 @@ function shouldRunGuardsAndResolvers( } function deactivateRouteAndItsChildren( - route: TreeNode, context: OutletContext|null, checks: Checks): void { + route: TreeNode, + context: OutletContext | null, + checks: Checks, +): void { const children = nodeChildrenAsMap(route); const r = route.value; diff --git a/packages/router/src/utils/tree.ts b/packages/router/src/utils/tree.ts index 91b607c6f7f..804df408732 100644 --- a/packages/router/src/utils/tree.ts +++ b/packages/router/src/utils/tree.ts @@ -21,7 +21,7 @@ export class Tree { /** * @internal */ - parent(t: T): T|null { + parent(t: T): T | null { const p = this.pathFromRoot(t); return p.length > 1 ? p[p.length - 2] : null; } @@ -31,13 +31,13 @@ export class Tree { */ children(t: T): T[] { const n = findNode(t, this._root); - return n ? n.children.map(t => t.value) : []; + return n ? n.children.map((t) => t.value) : []; } /** * @internal */ - firstChild(t: T): T|null { + firstChild(t: T): T | null { const n = findNode(t, this._root); return n && n.children.length > 0 ? n.children[0].value : null; } @@ -49,21 +49,20 @@ export class Tree { const p = findPath(t, this._root); if (p.length < 2) return []; - const c = p[p.length - 2].children.map(c => c.value); - return c.filter(cc => cc !== t); + const c = p[p.length - 2].children.map((c) => c.value); + return c.filter((cc) => cc !== t); } /** * @internal */ pathFromRoot(t: T): T[] { - return findPath(t, this._root).map(s => s.value); + return findPath(t, this._root).map((s) => s.value); } } - // DFS for the node matching the value -function findNode(value: T, node: TreeNode): TreeNode|null { +function findNode(value: T, node: TreeNode): TreeNode | null { if (value === node.value) return node; for (const child of node.children) { @@ -90,7 +89,10 @@ function findPath(value: T, node: TreeNode): TreeNode[] { } export class TreeNode { - constructor(public value: T, public children: TreeNode[]) {} + constructor( + public value: T, + public children: TreeNode[], + ) {} toString(): string { return `TreeNode(${this.value})`; @@ -98,11 +100,11 @@ export class TreeNode { } // Return the list of T indexed by outlet name -export function nodeChildrenAsMap(node: TreeNode|null) { +export function nodeChildrenAsMap(node: TreeNode | null) { const map: {[outlet: string]: TreeNode} = {}; if (node) { - node.children.forEach(child => map[child.value.outlet] = child); + node.children.forEach((child) => (map[child.value.outlet] = child)); } return map; diff --git a/packages/router/src/utils/type_guards.ts b/packages/router/src/utils/type_guards.ts index 50ba44d741a..d1a856a0595 100644 --- a/packages/router/src/utils/type_guards.ts +++ b/packages/router/src/utils/type_guards.ts @@ -9,7 +9,11 @@ import {EmptyError} from 'rxjs'; import {CanActivateChildFn, CanActivateFn, CanDeactivateFn, CanLoadFn, CanMatchFn} from '../models'; -import {NAVIGATION_CANCELING_ERROR, NavigationCancelingError, RedirectingNavigationCancelingError} from '../navigation_canceling_error'; +import { + NAVIGATION_CANCELING_ERROR, + NavigationCancelingError, + RedirectingNavigationCancelingError, +} from '../navigation_canceling_error'; import {isUrlTree} from '../url_tree'; /** diff --git a/packages/router/src/utils/view_transition.ts b/packages/router/src/utils/view_transition.ts index 24b656ab460..e9ee42cfc65 100644 --- a/packages/router/src/utils/view_transition.ts +++ b/packages/router/src/utils/view_transition.ts @@ -9,15 +9,22 @@ /// import {DOCUMENT} from '@angular/common'; -import {afterNextRender, InjectionToken, Injector, NgZone, runInInjectionContext} from '@angular/core'; +import { + afterNextRender, + InjectionToken, + Injector, + NgZone, + runInInjectionContext, +} from '@angular/core'; import {ActivatedRouteSnapshot} from '../router_state'; -export const CREATE_VIEW_TRANSITION = - new InjectionToken(ngDevMode ? 'view transition helper' : ''); -export const VIEW_TRANSITION_OPTIONS = - new InjectionToken( - ngDevMode ? 'view transition options' : ''); +export const CREATE_VIEW_TRANSITION = new InjectionToken( + ngDevMode ? 'view transition helper' : '', +); +export const VIEW_TRANSITION_OPTIONS = new InjectionToken< + ViewTransitionsFeatureOptions & {skipNextTransition: boolean} +>(ngDevMode ? 'view transition options' : ''); /** * Options to configure the View Transitions integration in the Router. @@ -60,19 +67,19 @@ export interface ViewTransitionInfo { /** * @see https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition/finished */ - finished: Promise, + finished: Promise; /** * @see https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition/ready */ - ready: Promise, + ready: Promise; /** * @see https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition/updateCallbackDone */ - updateCallbackDone: Promise, + updateCallbackDone: Promise; /** * @see https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition/skipTransition */ - skipTransition(): void, + skipTransition(): void; }; /** * The `ActivatedRouteSnapshot` that the navigation is transitioning from. @@ -91,7 +98,10 @@ export interface ViewTransitionInfo { * @returns A Promise that resolves when the view transition callback begins. */ export function createViewTransition( - injector: Injector, from: ActivatedRouteSnapshot, to: ActivatedRouteSnapshot): Promise { + injector: Injector, + from: ActivatedRouteSnapshot, + to: ActivatedRouteSnapshot, +): Promise { const transitionOptions = injector.get(VIEW_TRANSITION_OPTIONS); const document = injector.get(DOCUMENT); // Create promises outside the Angular zone to avoid causing extra change detections @@ -125,7 +135,7 @@ export function createViewTransition( * Creates a promise that resolves after next render. */ function createRenderPromise(injector: Injector) { - return new Promise(resolve => { + return new Promise((resolve) => { afterNextRender(resolve, {injector}); }); } diff --git a/packages/router/test/apply_redirects.spec.ts b/packages/router/test/apply_redirects.spec.ts index e1bb3500316..b5d5fd5fccc 100644 --- a/packages/router/test/apply_redirects.spec.ts +++ b/packages/router/test/apply_redirects.spec.ts @@ -14,7 +14,13 @@ import {delay, map, tap} from 'rxjs/operators'; import {Route, Routes} from '../src/models'; import {recognize} from '../src/recognize'; import {RouterConfigLoader} from '../src/router_config_loader'; -import {DefaultUrlSerializer, equalSegments, UrlSegment, UrlSegmentGroup, UrlTree} from '../src/url_tree'; +import { + DefaultUrlSerializer, + equalSegments, + UrlSegment, + UrlSegmentGroup, + UrlTree, +} from '../src/url_tree'; import {getLoadedRoutes, getProvidersInjector} from '../src/utils/config'; describe('redirects', () => { @@ -27,26 +33,31 @@ describe('redirects', () => { it('should return the same url tree when no redirects', () => { checkRedirect( - [ - { - path: 'a', - component: ComponentA, - children: [ - {path: 'b', component: ComponentB}, - ], - }, - ], - '/a/b', (t: UrlTree) => { - expectTreeToBe(t, '/a/b'); - }); + [ + { + path: 'a', + component: ComponentA, + children: [{path: 'b', component: ComponentB}], + }, + ], + '/a/b', + (t: UrlTree) => { + expectTreeToBe(t, '/a/b'); + }, + ); }); it('should add new segments when needed', () => { checkRedirect( - [{path: 'a/b', redirectTo: 'a/b/c'}, {path: '**', component: ComponentC}], '/a/b', - (t: UrlTree) => { - expectTreeToBe(t, '/a/b/c'); - }); + [ + {path: 'a/b', redirectTo: 'a/b/c'}, + {path: '**', component: ComponentC}, + ], + '/a/b', + (t: UrlTree) => { + expectTreeToBe(t, '/a/b/c'); + }, + ); }); it('should support redirecting with to an URL with query parameters', () => { @@ -62,146 +73,183 @@ describe('redirects', () => { it('should handle positional parameters', () => { checkRedirect( - [ - {path: 'a/:aid/b/:bid', redirectTo: 'newa/:aid/newb/:bid'}, - {path: '**', component: ComponentC} - ], - '/a/1/b/2', (t: UrlTree) => { - expectTreeToBe(t, '/newa/1/newb/2'); - }); + [ + {path: 'a/:aid/b/:bid', redirectTo: 'newa/:aid/newb/:bid'}, + {path: '**', component: ComponentC}, + ], + '/a/1/b/2', + (t: UrlTree) => { + expectTreeToBe(t, '/newa/1/newb/2'); + }, + ); }); it('should throw when cannot handle a positional parameter', () => { recognize( - testModule.injector, null!, null, - [ - {path: 'a/:id', redirectTo: 'a/:other'}, - ], - tree('/a/1'), serializer) - .subscribe(() => {}, (e) => { - expect(e.message).toContain('Cannot redirect to \'a/:other\'. Cannot find \':other\'.'); - }); + testModule.injector, + null!, + null, + [{path: 'a/:id', redirectTo: 'a/:other'}], + tree('/a/1'), + serializer, + ).subscribe( + () => {}, + (e) => { + expect(e.message).toContain("Cannot redirect to 'a/:other'. Cannot find ':other'."); + }, + ); }); it('should pass matrix parameters', () => { checkRedirect( - [{path: 'a/:id', redirectTo: 'd/a/:id/e'}, {path: '**', component: ComponentC}], - '/a;p1=1/1;p2=2', (t: UrlTree) => { - expectTreeToBe(t, '/d/a;p1=1/1;p2=2/e'); - }); + [ + {path: 'a/:id', redirectTo: 'd/a/:id/e'}, + {path: '**', component: ComponentC}, + ], + '/a;p1=1/1;p2=2', + (t: UrlTree) => { + expectTreeToBe(t, '/d/a;p1=1/1;p2=2/e'); + }, + ); }); it('should handle preserve secondary routes', () => { checkRedirect( - [ - {path: 'a/:id', redirectTo: 'd/a/:id/e'}, - {path: 'c/d', component: ComponentA, outlet: 'aux'}, {path: '**', component: ComponentC} - ], - '/a/1(aux:c/d)', (t: UrlTree) => { - expectTreeToBe(t, '/d/a/1/e(aux:c/d)'); - }); + [ + {path: 'a/:id', redirectTo: 'd/a/:id/e'}, + {path: 'c/d', component: ComponentA, outlet: 'aux'}, + {path: '**', component: ComponentC}, + ], + '/a/1(aux:c/d)', + (t: UrlTree) => { + expectTreeToBe(t, '/d/a/1/e(aux:c/d)'); + }, + ); }); it('should redirect secondary routes', () => { checkRedirect( - [ - {path: 'a/:id', component: ComponentA}, - {path: 'c/d', redirectTo: 'f/c/d/e', outlet: 'aux'}, - {path: '**', component: ComponentC, outlet: 'aux'} - ], - '/a/1(aux:c/d)', (t: UrlTree) => { - expectTreeToBe(t, '/a/1(aux:f/c/d/e)'); - }); + [ + {path: 'a/:id', component: ComponentA}, + {path: 'c/d', redirectTo: 'f/c/d/e', outlet: 'aux'}, + {path: '**', component: ComponentC, outlet: 'aux'}, + ], + '/a/1(aux:c/d)', + (t: UrlTree) => { + expectTreeToBe(t, '/a/1(aux:f/c/d/e)'); + }, + ); }); it('should use the configuration of the route redirected to', () => { checkRedirect( - [ - { - path: 'a', - component: ComponentA, - children: [ - {path: 'b', component: ComponentB}, - ] - }, - {path: 'c', redirectTo: 'a'} - ], - 'c/b', (t: UrlTree) => { - expectTreeToBe(t, 'a/b'); - }); + [ + { + path: 'a', + component: ComponentA, + children: [{path: 'b', component: ComponentB}], + }, + {path: 'c', redirectTo: 'a'}, + ], + 'c/b', + (t: UrlTree) => { + expectTreeToBe(t, 'a/b'); + }, + ); }); it('should support redirects with both main and aux', () => { checkRedirect( - [{ + [ + { path: 'a', children: [ - {path: 'bb', component: ComponentB}, {path: 'b', redirectTo: 'bb'}, + {path: 'bb', component: ComponentB}, + {path: 'b', redirectTo: 'bb'}, {path: 'cc', component: ComponentC, outlet: 'aux'}, - {path: 'b', redirectTo: 'cc', outlet: 'aux'} - ] - }], - 'a/(b//aux:b)', (t: UrlTree) => { - expectTreeToBe(t, 'a/(bb//aux:cc)'); - }); + {path: 'b', redirectTo: 'cc', outlet: 'aux'}, + ], + }, + ], + 'a/(b//aux:b)', + (t: UrlTree) => { + expectTreeToBe(t, 'a/(bb//aux:cc)'); + }, + ); }); it('should support redirects with both main and aux (with a nested redirect)', () => { checkRedirect( - [{ + [ + { path: 'a', children: [ - {path: 'bb', component: ComponentB}, {path: 'b', redirectTo: 'bb'}, { + {path: 'bb', component: ComponentB}, + {path: 'b', redirectTo: 'bb'}, + { path: 'cc', component: ComponentC, outlet: 'aux', - children: [{path: 'dd', component: ComponentC}, {path: 'd', redirectTo: 'dd'}] + children: [ + {path: 'dd', component: ComponentC}, + {path: 'd', redirectTo: 'dd'}, + ], }, - {path: 'b', redirectTo: 'cc/d', outlet: 'aux'} - ] - }], - 'a/(b//aux:b)', (t: UrlTree) => { - expectTreeToBe(t, 'a/(bb//aux:cc/dd)'); - }); + {path: 'b', redirectTo: 'cc/d', outlet: 'aux'}, + ], + }, + ], + 'a/(b//aux:b)', + (t: UrlTree) => { + expectTreeToBe(t, 'a/(bb//aux:cc/dd)'); + }, + ); }); it('should redirect wild cards', () => { checkRedirect( - [ - {path: '404', component: ComponentA}, - {path: '**', redirectTo: '/404'}, - ], - '/a/1(aux:c/d)', (t: UrlTree) => { - expectTreeToBe(t, '/404'); - }); + [ + {path: '404', component: ComponentA}, + {path: '**', redirectTo: '/404'}, + ], + '/a/1(aux:c/d)', + (t: UrlTree) => { + expectTreeToBe(t, '/404'); + }, + ); }); it('should throw an error on infinite absolute redirect', () => { recognize( - TestBed.inject(EnvironmentInjector), TestBed.inject(RouterConfigLoader), null, - [{path: '**', redirectTo: '/404'}], tree('/'), new DefaultUrlSerializer()) - .subscribe({ - next: () => fail('expected infinite redirect error'), - error: e => { - expect((e as Error).message).toMatch(/infinite redirect/); - } - }); + TestBed.inject(EnvironmentInjector), + TestBed.inject(RouterConfigLoader), + null, + [{path: '**', redirectTo: '/404'}], + tree('/'), + new DefaultUrlSerializer(), + ).subscribe({ + next: () => fail('expected infinite redirect error'), + error: (e) => { + expect((e as Error).message).toMatch(/infinite redirect/); + }, + }); }); - it('should support absolute redirects', () => { checkRedirect( - [ - { - path: 'a', - component: ComponentA, - children: [{path: 'b/:id', redirectTo: '/absolute/:id?a=1&b=:b#f1'}] - }, - {path: '**', component: ComponentC} - ], - '/a/b/1?b=2', (t: UrlTree) => { - expectTreeToBe(t, '/absolute/1?a=1&b=2#f1'); - }); + [ + { + path: 'a', + component: ComponentA, + children: [{path: 'b/:id', redirectTo: '/absolute/:id?a=1&b=:b#f1'}], + }, + {path: '**', component: ComponentC}, + ], + '/a/b/1?b=2', + (t: UrlTree) => { + expectTreeToBe(t, '/absolute/1?a=1&b=2#f1'); + }, + ); }); it('should not create injector for Route if the route does not match', () => { @@ -210,9 +258,7 @@ describe('redirects', () => { { path: 'a', component: ComponentA, - children: [ - {path: 'b', component: ComponentB}, - ], + children: [{path: 'b', component: ComponentB}], }, ]; checkRedirect(routes, '/a/b', (t: UrlTree) => { @@ -239,7 +285,7 @@ describe('redirects', () => { expect(getProvidersInjector(routes[0])).toBeDefined(); // The second `Route` did not match at all so we should not create an injector for it expect(getProvidersInjector(routes[1])).not.toBeDefined(); - } + }, }); }); @@ -262,7 +308,7 @@ describe('redirects', () => { path: 'a', component: ComponentA, providers: [], - } + }, ]; recognize(testModule.injector, null!, null, routes, tree('a'), serializer).subscribe({ next: () => { @@ -274,7 +320,7 @@ describe('redirects', () => { }, error: () => { throw 'Should not be reached'; - } + }, }); }); @@ -282,149 +328,186 @@ describe('redirects', () => { it('should load config on demand', () => { const loadedConfig = { routes: [{path: 'b', component: ComponentB}], - injector: testModule.injector + injector: testModule.injector, }; const loader: Pick = { loadChildren: (injector: any, p: any) => { if (injector !== testModule.injector) throw 'Invalid Injector'; return of(loadedConfig); - } + }, }; - const config: Routes = - [{path: 'a', component: ComponentA, loadChildren: jasmine.createSpy('children')}]; + const config: Routes = [ + {path: 'a', component: ComponentA, loadChildren: jasmine.createSpy('children')}, + ]; - recognize(testModule.injector, loader, null, config, tree('a/b'), serializer) - .forEach(({tree}) => { - expectTreeToBe(tree, '/a/b'); - expect(getLoadedRoutes(config[0])).toBe(loadedConfig.routes); - }); + recognize(testModule.injector, loader, null, config, tree('a/b'), serializer).forEach( + ({tree}) => { + expectTreeToBe(tree, '/a/b'); + expect(getLoadedRoutes(config[0])).toBe(loadedConfig.routes); + }, + ); }); it('should handle the case when the loader errors', () => { const loader: Pick = { - loadChildren: (p: any) => new Observable((obs) => obs.error(new Error('Loading Error'))) + loadChildren: (p: any) => new Observable((obs) => obs.error(new Error('Loading Error'))), }; - const config = - [{path: 'a', component: ComponentA, loadChildren: jasmine.createSpy('children')}]; + const config = [ + {path: 'a', component: ComponentA, loadChildren: jasmine.createSpy('children')}, + ]; - recognize(testModule.injector, loader, null, config, tree('a/b'), serializer) - .subscribe(() => {}, (e) => { - expect(e.message).toEqual('Loading Error'); - }); + recognize(testModule.injector, loader, null, config, tree('a/b'), serializer).subscribe( + () => {}, + (e) => { + expect(e.message).toEqual('Loading Error'); + }, + ); }); it('should load when all canLoad guards return true', () => { const loadedConfig = { routes: [{path: 'b', component: ComponentB}], - injector: testModule.injector + injector: testModule.injector, }; const loader: Pick = { - loadChildren: (injector: any, p: any) => of(loadedConfig) + loadChildren: (injector: any, p: any) => of(loadedConfig), }; - const config = [{ - path: 'a', - component: ComponentA, - canLoad: [() => true, () => true], - loadChildren: jasmine.createSpy('children') - }]; + const config = [ + { + path: 'a', + component: ComponentA, + canLoad: [() => true, () => true], + loadChildren: jasmine.createSpy('children'), + }, + ]; recognize( - TestBed.inject(EnvironmentInjector), loader, null, config, tree('a/b'), serializer) - .forEach(({tree: r}) => { - expectTreeToBe(r, '/a/b'); - }); + TestBed.inject(EnvironmentInjector), + loader, + null, + config, + tree('a/b'), + serializer, + ).forEach(({tree: r}) => { + expectTreeToBe(r, '/a/b'); + }); }); it('should not load when any canLoad guards return false', () => { const loadedConfig = { routes: [{path: 'b', component: ComponentB}], - injector: testModule.injector + injector: testModule.injector, }; const loader: Pick = { - loadChildren: (injector: any, p: any) => of(loadedConfig) + loadChildren: (injector: any, p: any) => of(loadedConfig), }; - const config = [{ - path: 'a', - component: ComponentA, - canLoad: [() => true, () => false], - loadChildren: jasmine.createSpy('children') - }]; + const config = [ + { + path: 'a', + component: ComponentA, + canLoad: [() => true, () => false], + loadChildren: jasmine.createSpy('children'), + }, + ]; recognize( - TestBed.inject(EnvironmentInjector), loader, null, config, tree('a/b'), serializer) - .subscribe( - () => { - throw 'Should not reach'; - }, - (e) => { - expect(e.message).toEqual( - `NavigationCancelingError: Cannot load children because the guard of the route "path: 'a'" returned false`); - }); + TestBed.inject(EnvironmentInjector), + loader, + null, + config, + tree('a/b'), + serializer, + ).subscribe( + () => { + throw 'Should not reach'; + }, + (e) => { + expect(e.message).toEqual( + `NavigationCancelingError: Cannot load children because the guard of the route "path: 'a'" returned false`, + ); + }, + ); }); it('should not load when any canLoad guards is rejected (promises)', () => { const loadedConfig = { routes: [{path: 'b', component: ComponentB}], - injector: testModule.injector + injector: testModule.injector, }; const loader: Pick = { - loadChildren: (injector: any, p: any) => of(loadedConfig) + loadChildren: (injector: any, p: any) => of(loadedConfig), }; - const config = [{ - path: 'a', - component: ComponentA, - canLoad: [() => Promise.resolve(true), () => Promise.reject('someError')], - loadChildren: jasmine.createSpy('children') - }]; + const config = [ + { + path: 'a', + component: ComponentA, + canLoad: [() => Promise.resolve(true), () => Promise.reject('someError')], + loadChildren: jasmine.createSpy('children'), + }, + ]; recognize( - TestBed.inject(EnvironmentInjector), loader, null, config, tree('a/b'), serializer) - .subscribe( - () => { - throw 'Should not reach'; - }, - (e) => { - expect(e).toEqual('someError'); - }); + TestBed.inject(EnvironmentInjector), + loader, + null, + config, + tree('a/b'), + serializer, + ).subscribe( + () => { + throw 'Should not reach'; + }, + (e) => { + expect(e).toEqual('someError'); + }, + ); }); it('should work with objects implementing the CanLoad interface', () => { const loadedConfig = { routes: [{path: 'b', component: ComponentB}], - injector: testModule.injector + injector: testModule.injector, }; const loader: Pick = { - loadChildren: (injector: any, p: any) => of(loadedConfig) + loadChildren: (injector: any, p: any) => of(loadedConfig), }; - const config = [{ - path: 'a', - component: ComponentA, - canLoad: [() => Promise.resolve(true)], - loadChildren: jasmine.createSpy('children') - }]; + const config = [ + { + path: 'a', + component: ComponentA, + canLoad: [() => Promise.resolve(true)], + loadChildren: jasmine.createSpy('children'), + }, + ]; recognize( - TestBed.inject(EnvironmentInjector), loader, null, config, tree('a/b'), serializer) - .subscribe( - ({tree: r}) => { - expectTreeToBe(r, '/a/b'); - }, - (e) => { - throw 'Should not reach'; - }); + TestBed.inject(EnvironmentInjector), + loader, + null, + config, + tree('a/b'), + serializer, + ).subscribe( + ({tree: r}) => { + expectTreeToBe(r, '/a/b'); + }, + (e) => { + throw 'Should not reach'; + }, + ); }); it('should pass UrlSegments to functions implementing the canLoad guard interface', () => { const loadedConfig = { routes: [{path: 'b', component: ComponentB}], - injector: testModule.injector + injector: testModule.injector, }; const loader: Pick = { - loadChildren: (injector: any, p: any) => of(loadedConfig) + loadChildren: (injector: any, p: any) => of(loadedConfig), }; let passedUrlSegments: UrlSegment[]; @@ -434,34 +517,42 @@ describe('redirects', () => { return true; }; - const config = [{ - path: 'a', - component: ComponentA, - canLoad: [guard], - loadChildren: jasmine.createSpy('children') - }]; + const config = [ + { + path: 'a', + component: ComponentA, + canLoad: [guard], + loadChildren: jasmine.createSpy('children'), + }, + ]; recognize( - TestBed.inject(EnvironmentInjector), loader, null, config, tree('a/b'), serializer) - .subscribe( - ({tree: r}) => { - expectTreeToBe(r, '/a/b'); - expect(passedUrlSegments.length).toBe(2); - expect(passedUrlSegments[0].path).toBe('a'); - expect(passedUrlSegments[1].path).toBe('b'); - }, - (e) => { - throw 'Should not reach'; - }); + TestBed.inject(EnvironmentInjector), + loader, + null, + config, + tree('a/b'), + serializer, + ).subscribe( + ({tree: r}) => { + expectTreeToBe(r, '/a/b'); + expect(passedUrlSegments.length).toBe(2); + expect(passedUrlSegments[0].path).toBe('a'); + expect(passedUrlSegments[1].path).toBe('b'); + }, + (e) => { + throw 'Should not reach'; + }, + ); }); it('should pass UrlSegments to objects implementing the canLoad guard interface', () => { const loadedConfig = { routes: [{path: 'b', component: ComponentB}], - injector: testModule.injector + injector: testModule.injector, }; const loader: Pick = { - loadChildren: (injector: any, p: any) => of(loadedConfig) + loadChildren: (injector: any, p: any) => of(loadedConfig), }; let passedUrlSegments: UrlSegment[]; @@ -470,56 +561,59 @@ describe('redirects', () => { canLoad: (route: Route, urlSegments: UrlSegment[]) => { passedUrlSegments = urlSegments; return true; - } + }, }; - const injector = {get: (token: any) => token === 'guard' ? guard : {injector}}; + const injector = {get: (token: any) => (token === 'guard' ? guard : {injector})}; - const config = [{ - path: 'a', - component: ComponentA, - canLoad: ['guard'], - loadChildren: jasmine.createSpy('children') - }]; + const config = [ + { + path: 'a', + component: ComponentA, + canLoad: ['guard'], + loadChildren: jasmine.createSpy('children'), + }, + ]; - recognize(injector, loader, null, config, tree('a/b'), serializer) - .subscribe( - ({tree: r}) => { - expectTreeToBe(r, '/a/b'); - expect(passedUrlSegments.length).toBe(2); - expect(passedUrlSegments[0].path).toBe('a'); - expect(passedUrlSegments[1].path).toBe('b'); - }, - (e) => { - throw 'Should not reach'; - }); + recognize(injector, loader, null, config, tree('a/b'), serializer).subscribe( + ({tree: r}) => { + expectTreeToBe(r, '/a/b'); + expect(passedUrlSegments.length).toBe(2); + expect(passedUrlSegments[0].path).toBe('a'); + expect(passedUrlSegments[1].path).toBe('b'); + }, + (e) => { + throw 'Should not reach'; + }, + ); }); it('should work with absolute redirects', () => { const loadedConfig = { routes: [{path: '', component: ComponentB}], - injector: testModule.injector + injector: testModule.injector, }; const loader: Pick = { - loadChildren: (injector: any, p: any) => of(loadedConfig) + loadChildren: (injector: any, p: any) => of(loadedConfig), }; const config: Routes = [ {path: '', pathMatch: 'full', redirectTo: '/a'}, - {path: 'a', loadChildren: jasmine.createSpy('children')} + {path: 'a', loadChildren: jasmine.createSpy('children')}, ]; - recognize(testModule.injector, loader, null, config, tree(''), serializer) - .forEach(({tree: r}) => { - expectTreeToBe(r, 'a'); - expect(getLoadedRoutes(config[1])).toBe(loadedConfig.routes); - }); + recognize(testModule.injector, loader, null, config, tree(''), serializer).forEach( + ({tree: r}) => { + expectTreeToBe(r, 'a'); + expect(getLoadedRoutes(config[1])).toBe(loadedConfig.routes); + }, + ); }); it('should load the configuration only once', () => { const loadedConfig = { routes: [{path: '', component: ComponentB}], - injector: testModule.injector + injector: testModule.injector, }; let called = false; @@ -528,51 +622,55 @@ describe('redirects', () => { if (called) throw new Error('Should not be called twice'); called = true; return of(loadedConfig); - } + }, }; const config: Routes = [{path: 'a', loadChildren: jasmine.createSpy('children')}]; - recognize(testModule.injector, loader, null, config, tree('a?k1'), serializer) - .subscribe(r => {}); + recognize(testModule.injector, loader, null, config, tree('a?k1'), serializer).subscribe( + (r) => {}, + ); - recognize(testModule.injector, loader, null, config, tree('a?k2'), serializer) - .subscribe( - ({tree: r}) => { - expectTreeToBe(r, 'a?k2'); - expect(getLoadedRoutes(config[0])).toBe(loadedConfig.routes); - }, - (e) => { - throw 'Should not reach'; - }); + recognize(testModule.injector, loader, null, config, tree('a?k2'), serializer).subscribe( + ({tree: r}) => { + expectTreeToBe(r, 'a?k2'); + expect(getLoadedRoutes(config[0])).toBe(loadedConfig.routes); + }, + (e) => { + throw 'Should not reach'; + }, + ); }); it('should load the configuration of a wildcard route', () => { const loadedConfig = { routes: [{path: '', component: ComponentB}], - injector: testModule.injector + injector: testModule.injector, }; const loader: Pick = { - loadChildren: (injector: any, p: any) => of(loadedConfig) + loadChildren: (injector: any, p: any) => of(loadedConfig), }; const config: Routes = [{path: '**', loadChildren: jasmine.createSpy('children')}]; - recognize(testModule.injector, loader, null, config, tree('xyz'), serializer) - .forEach(({tree: r}) => { - expect(getLoadedRoutes(config[0])).toBe(loadedConfig.routes); - }); + recognize(testModule.injector, loader, null, config, tree('xyz'), serializer).forEach( + ({tree: r}) => { + expect(getLoadedRoutes(config[0])).toBe(loadedConfig.routes); + }, + ); }); it('should not load the configuration of a wildcard route if there is a match', () => { const loadedConfig = { routes: [{path: '', component: ComponentB}], - injector: testModule.injector + injector: testModule.injector, }; - const loader: jasmine.SpyObj> = - jasmine.createSpyObj('loader', ['loadChildren']); + const loader: jasmine.SpyObj> = jasmine.createSpyObj( + 'loader', + ['loadChildren'], + ); loader.loadChildren.and.returnValue(of(loadedConfig).pipe(delay(0))); const config: Routes = [ @@ -580,152 +678,167 @@ describe('redirects', () => { {path: '**', loadChildren: jasmine.createSpy('children')}, ]; - recognize(testModule.injector, loader, null, config, tree(''), serializer) - .forEach(({tree: r}) => { - expect(loader.loadChildren.calls.count()).toEqual(1); - expect(loader.loadChildren.calls.first().args).not.toContain(jasmine.objectContaining({ - loadChildren: jasmine.createSpy('children') - })); - }); + recognize(testModule.injector, loader, null, config, tree(''), serializer).forEach( + ({tree: r}) => { + expect(loader.loadChildren.calls.count()).toEqual(1); + expect(loader.loadChildren.calls.first().args).not.toContain( + jasmine.objectContaining({ + loadChildren: jasmine.createSpy('children'), + }), + ); + }, + ); }); it('should load the configuration after a local redirect from a wildcard route', () => { const loadedConfig = { routes: [{path: '', component: ComponentB}], - injector: testModule.injector + injector: testModule.injector, }; const loader: Pick = { - loadChildren: (injector: any, p: any) => of(loadedConfig) + loadChildren: (injector: any, p: any) => of(loadedConfig), }; const config: Routes = [ {path: 'not-found', loadChildren: jasmine.createSpy('children')}, - {path: '**', redirectTo: 'not-found'} + {path: '**', redirectTo: 'not-found'}, ]; - recognize(testModule.injector, loader, null, config, tree('xyz'), serializer) - .forEach(({tree: r}) => { - expect(getLoadedRoutes(config[0])).toBe(loadedConfig.routes); - }); + recognize(testModule.injector, loader, null, config, tree('xyz'), serializer).forEach( + ({tree: r}) => { + expect(getLoadedRoutes(config[0])).toBe(loadedConfig.routes); + }, + ); }); it('should load the configuration after an absolute redirect from a wildcard route', () => { const loadedConfig = { routes: [{path: '', component: ComponentB}], - injector: testModule.injector + injector: testModule.injector, }; const loader: Pick = { - loadChildren: (injector: any, p: any) => of(loadedConfig) + loadChildren: (injector: any, p: any) => of(loadedConfig), }; const config: Routes = [ {path: 'not-found', loadChildren: jasmine.createSpy('children')}, - {path: '**', redirectTo: '/not-found'} + {path: '**', redirectTo: '/not-found'}, ]; - recognize(testModule.injector, loader, null, config, tree('xyz'), serializer) - .forEach(({tree: r}) => { - expect(getLoadedRoutes(config[0])).toBe(loadedConfig.routes); - }); + recognize(testModule.injector, loader, null, config, tree('xyz'), serializer).forEach( + ({tree: r}) => { + expect(getLoadedRoutes(config[0])).toBe(loadedConfig.routes); + }, + ); }); - it('should load all matching configurations of empty path, including an auxiliary outlets', - fakeAsync(() => { - const loadedConfig = { - routes: [{path: '', component: ComponentA}], - injector: testModule.injector - }; - let loadCalls = 0; - let loaded: string[] = []; - const loader: Pick = { - loadChildren: (injector: any, p: Route) => { - loadCalls++; - return of(loadedConfig) - .pipe( - delay(100 * loadCalls), - tap(() => loaded.push((p.loadChildren as jasmine.Spy).and.identity)), - ); - } - }; - - const config: Routes = [ - {path: '', loadChildren: jasmine.createSpy('root')}, - {path: '', loadChildren: jasmine.createSpy('aux'), outlet: 'popup'} - ]; - - recognize(testModule.injector, loader, null, config, tree(''), serializer) - .subscribe(); - expect(loadCalls).toBe(1); - tick(100); - expect(loaded).toEqual(['root']); - expect(loadCalls).toBe(2); - tick(200); - expect(loaded).toEqual(['root', 'aux']); - })); - - it('should not try to load any matching configuration if previous load completed', - fakeAsync(() => { - const loadedConfig = { - routes: [{path: 'a', component: ComponentA}], - injector: testModule.injector - }; - let loadCalls = 0; - let loaded: string[] = []; - const loader: Pick = { - loadChildren: (injector: any, p: Route) => { - loadCalls++; - return of(loadedConfig) - .pipe( - delay(100 * loadCalls), - tap(() => loaded.push((p.loadChildren as jasmine.Spy).and.identity)), - ); - } - }; - - const config: Routes = [ - {path: '**', loadChildren: jasmine.createSpy('children')}, - ]; - - recognize(testModule.injector, loader, null, config, tree('xyz/a'), serializer) - .subscribe(); - expect(loadCalls).toBe(1); - tick(50); - expect(loaded).toEqual([]); - recognize(testModule.injector, loader, null, config, tree('xyz/b'), serializer) - .subscribe(); - tick(50); - expect(loaded).toEqual(['children']); - expect(loadCalls).toBe(2); - tick(200); - recognize(testModule.injector, loader, null, config, tree('xyz/c'), serializer) - .subscribe(); - tick(50); - expect(loadCalls).toBe(2); - tick(300); - })); - - it('loads only the first match when two Routes with the same outlet have the same path', () => { + it('should load all matching configurations of empty path, including an auxiliary outlets', fakeAsync(() => { const loadedConfig = { routes: [{path: '', component: ComponentA}], - injector: testModule.injector + injector: testModule.injector, }; let loadCalls = 0; let loaded: string[] = []; const loader: Pick = { loadChildren: (injector: any, p: Route) => { loadCalls++; - return of(loadedConfig) - .pipe( - tap(() => loaded.push((p.loadChildren as jasmine.Spy).and.identity)), - ); - } + return of(loadedConfig).pipe( + delay(100 * loadCalls), + tap(() => loaded.push((p.loadChildren as jasmine.Spy).and.identity)), + ); + }, + }; + + const config: Routes = [ + {path: '', loadChildren: jasmine.createSpy('root')}, + {path: '', loadChildren: jasmine.createSpy('aux'), outlet: 'popup'}, + ]; + + recognize(testModule.injector, loader, null, config, tree(''), serializer).subscribe(); + expect(loadCalls).toBe(1); + tick(100); + expect(loaded).toEqual(['root']); + expect(loadCalls).toBe(2); + tick(200); + expect(loaded).toEqual(['root', 'aux']); + })); + + it('should not try to load any matching configuration if previous load completed', fakeAsync(() => { + const loadedConfig = { + routes: [{path: 'a', component: ComponentA}], + injector: testModule.injector, + }; + let loadCalls = 0; + let loaded: string[] = []; + const loader: Pick = { + loadChildren: (injector: any, p: Route) => { + loadCalls++; + return of(loadedConfig).pipe( + delay(100 * loadCalls), + tap(() => loaded.push((p.loadChildren as jasmine.Spy).and.identity)), + ); + }, + }; + + const config: Routes = [{path: '**', loadChildren: jasmine.createSpy('children')}]; + + recognize( + testModule.injector, + loader, + null, + config, + tree('xyz/a'), + serializer, + ).subscribe(); + expect(loadCalls).toBe(1); + tick(50); + expect(loaded).toEqual([]); + recognize( + testModule.injector, + loader, + null, + config, + tree('xyz/b'), + serializer, + ).subscribe(); + tick(50); + expect(loaded).toEqual(['children']); + expect(loadCalls).toBe(2); + tick(200); + recognize( + testModule.injector, + loader, + null, + config, + tree('xyz/c'), + serializer, + ).subscribe(); + tick(50); + expect(loadCalls).toBe(2); + tick(300); + })); + + it('loads only the first match when two Routes with the same outlet have the same path', () => { + const loadedConfig = { + routes: [{path: '', component: ComponentA}], + injector: testModule.injector, + }; + let loadCalls = 0; + let loaded: string[] = []; + const loader: Pick = { + loadChildren: (injector: any, p: Route) => { + loadCalls++; + return of(loadedConfig).pipe( + tap(() => loaded.push((p.loadChildren as jasmine.Spy).and.identity)), + ); + }, }; const config: Routes = [ {path: 'a', loadChildren: jasmine.createSpy('first')}, - {path: 'a', loadChildren: jasmine.createSpy('second')} + {path: 'a', loadChildren: jasmine.createSpy('second')}, ]; recognize(testModule.injector, loader, null, config, tree('a'), serializer).subscribe(); @@ -733,75 +846,78 @@ describe('redirects', () => { expect(loaded).toEqual(['first']); }); - it('should load the configuration of empty root path if the entry is an aux outlet', - fakeAsync(() => { - const loadedConfig = { - routes: [{path: '', component: ComponentA}], - injector: testModule.injector - }; - let loaded: string[] = []; - const rootDelay = 100; - const auxDelay = 1; - const loader: Pick = { - loadChildren: (injector: any, p: Route) => { - const delayMs = - (p.loadChildren! as jasmine.Spy).and.identity === 'aux' ? auxDelay : rootDelay; - return of(loadedConfig) - .pipe( - delay(delayMs), - tap(() => loaded.push((p.loadChildren as jasmine.Spy).and.identity)), - ); - } - }; + it('should load the configuration of empty root path if the entry is an aux outlet', fakeAsync(() => { + const loadedConfig = { + routes: [{path: '', component: ComponentA}], + injector: testModule.injector, + }; + let loaded: string[] = []; + const rootDelay = 100; + const auxDelay = 1; + const loader: Pick = { + loadChildren: (injector: any, p: Route) => { + const delayMs = + (p.loadChildren! as jasmine.Spy).and.identity === 'aux' ? auxDelay : rootDelay; + return of(loadedConfig).pipe( + delay(delayMs), + tap(() => loaded.push((p.loadChildren as jasmine.Spy).and.identity)), + ); + }, + }; - const config: Routes = [ - // Define aux route first so it matches before the primary outlet - {path: 'modal', loadChildren: jasmine.createSpy('aux'), outlet: 'popup'}, - {path: '', loadChildren: jasmine.createSpy('root')}, - ]; + const config: Routes = [ + // Define aux route first so it matches before the primary outlet + {path: 'modal', loadChildren: jasmine.createSpy('aux'), outlet: 'popup'}, + {path: '', loadChildren: jasmine.createSpy('root')}, + ]; - recognize( - testModule.injector, loader, null, config, tree('(popup:modal)'), serializer) - .subscribe(); - tick(auxDelay); - tick(rootDelay); - expect(loaded.sort()).toEqual(['aux', 'root'].sort()); - })); + recognize( + testModule.injector, + loader, + null, + config, + tree('(popup:modal)'), + serializer, + ).subscribe(); + tick(auxDelay); + tick(rootDelay); + expect(loaded.sort()).toEqual(['aux', 'root'].sort()); + })); }); describe('empty paths', () => { it('redirect from an empty path should work (local redirect)', () => { checkRedirect( - [ - { - path: 'a', - component: ComponentA, - children: [ - {path: 'b', component: ComponentB}, - ] - }, - {path: '', redirectTo: 'a'} - ], - 'b', (t: UrlTree) => { - expectTreeToBe(t, 'a/b'); - }); + [ + { + path: 'a', + component: ComponentA, + children: [{path: 'b', component: ComponentB}], + }, + {path: '', redirectTo: 'a'}, + ], + 'b', + (t: UrlTree) => { + expectTreeToBe(t, 'a/b'); + }, + ); }); it('redirect from an empty path should work (absolute redirect)', () => { checkRedirect( - [ - { - path: 'a', - component: ComponentA, - children: [ - {path: 'b', component: ComponentB}, - ] - }, - {path: '', redirectTo: '/a/b'} - ], - '', (t: UrlTree) => { - expectTreeToBe(t, 'a/b'); - }); + [ + { + path: 'a', + component: ComponentA, + children: [{path: 'b', component: ComponentB}], + }, + {path: '', redirectTo: '/a/b'}, + ], + '', + (t: UrlTree) => { + expectTreeToBe(t, 'a/b'); + }, + ); }); it('should redirect empty path route only when terminal', () => { @@ -809,160 +925,192 @@ describe('redirects', () => { { path: 'a', component: ComponentA, - children: [ - {path: 'b', component: ComponentB}, - ] + children: [{path: 'b', component: ComponentB}], }, - {path: '', redirectTo: 'a', pathMatch: 'full'} + {path: '', redirectTo: 'a', pathMatch: 'full'}, ]; - recognize(testModule.injector, null!, null, config, tree('b'), serializer) - .subscribe( - (_) => { - throw 'Should not be reached'; - }, - e => { - expect(e.message).toContain('Cannot match any routes. URL Segment: \'b\''); - }); + recognize(testModule.injector, null!, null, config, tree('b'), serializer).subscribe( + (_) => { + throw 'Should not be reached'; + }, + (e) => { + expect(e.message).toContain("Cannot match any routes. URL Segment: 'b'"); + }, + ); }); it('redirect from an empty path should work (nested case)', () => { checkRedirect( - [ - { - path: 'a', - component: ComponentA, - children: [{path: 'b', component: ComponentB}, {path: '', redirectTo: 'b'}] - }, - {path: '', redirectTo: 'a'} - ], - '', (t: UrlTree) => { - expectTreeToBe(t, 'a/b'); - }); + [ + { + path: 'a', + component: ComponentA, + children: [ + {path: 'b', component: ComponentB}, + {path: '', redirectTo: 'b'}, + ], + }, + {path: '', redirectTo: 'a'}, + ], + '', + (t: UrlTree) => { + expectTreeToBe(t, 'a/b'); + }, + ); }); it('redirect to an empty path should work', () => { checkRedirect( - [ - {path: '', component: ComponentA, children: [{path: 'b', component: ComponentB}]}, - {path: 'a', redirectTo: ''} - ], - 'a/b', (t: UrlTree) => { - expectTreeToBe(t, 'b'); - }); + [ + {path: '', component: ComponentA, children: [{path: 'b', component: ComponentB}]}, + {path: 'a', redirectTo: ''}, + ], + 'a/b', + (t: UrlTree) => { + expectTreeToBe(t, 'b'); + }, + ); }); describe('aux split is in the middle', () => { it('should create a new url segment (non-terminal)', () => { checkRedirect( - [{ + [ + { path: 'a', children: [ {path: 'b', component: ComponentB}, {path: 'c', component: ComponentC, outlet: 'aux'}, - {path: '', redirectTo: 'c', outlet: 'aux'} - ] - }], - 'a/b', (t: UrlTree) => { - expectTreeToBe(t, 'a/(b//aux:c)'); - }); + {path: '', redirectTo: 'c', outlet: 'aux'}, + ], + }, + ], + 'a/b', + (t: UrlTree) => { + expectTreeToBe(t, 'a/(b//aux:c)'); + }, + ); }); it('should create a new url segment (terminal)', () => { checkRedirect( - [{ + [ + { path: 'a', children: [ {path: 'b', component: ComponentB}, {path: 'c', component: ComponentC, outlet: 'aux'}, - {path: '', pathMatch: 'full', redirectTo: 'c', outlet: 'aux'} - ] - }], - 'a/b', (t: UrlTree) => { - expectTreeToBe(t, 'a/b'); - }); + {path: '', pathMatch: 'full', redirectTo: 'c', outlet: 'aux'}, + ], + }, + ], + 'a/b', + (t: UrlTree) => { + expectTreeToBe(t, 'a/b'); + }, + ); }); }); describe('aux split after empty path parent', () => { it('should work with non-empty auxiliary path', () => { checkRedirect( - [{ + [ + { path: '', children: [ {path: 'a', component: ComponentA}, {path: 'c', component: ComponentC, outlet: 'aux'}, - {path: 'b', redirectTo: 'c', outlet: 'aux'} - ] - }], - '(aux:b)', (t: UrlTree) => { - expectTreeToBe(t, '(aux:c)'); - }); + {path: 'b', redirectTo: 'c', outlet: 'aux'}, + ], + }, + ], + '(aux:b)', + (t: UrlTree) => { + expectTreeToBe(t, '(aux:c)'); + }, + ); }); it('should work with empty auxiliary path', () => { checkRedirect( - [{ + [ + { path: '', children: [ {path: 'a', component: ComponentA}, {path: 'c', component: ComponentC, outlet: 'aux'}, - {path: '', redirectTo: 'c', outlet: 'aux'} - ] - }], - '', (t: UrlTree) => { - expectTreeToBe(t, '(aux:c)'); - }); + {path: '', redirectTo: 'c', outlet: 'aux'}, + ], + }, + ], + '', + (t: UrlTree) => { + expectTreeToBe(t, '(aux:c)'); + }, + ); }); it('should work with empty auxiliary path and matching primary', () => { checkRedirect( - [{ + [ + { path: '', children: [ {path: 'a', component: ComponentA}, {path: 'c', component: ComponentC, outlet: 'aux'}, - {path: '', redirectTo: 'c', outlet: 'aux'} - ] - }], - 'a', (t: UrlTree) => { - expect(t.toString()).toEqual('/a(aux:c)'); - }); + {path: '', redirectTo: 'c', outlet: 'aux'}, + ], + }, + ], + 'a', + (t: UrlTree) => { + expect(t.toString()).toEqual('/a(aux:c)'); + }, + ); }); it('should work with aux outlets adjacent to and children of empty path at once', () => { checkRedirect( - [ - { - path: '', - component: ComponentA, - children: [{path: 'b', outlet: 'b', component: ComponentB}] - }, - {path: 'c', outlet: 'c', component: ComponentC} - ], - '(b:b//c:c)', (t: UrlTree) => { - expect(t.toString()).toEqual('/(b:b//c:c)'); - }); + [ + { + path: '', + component: ComponentA, + children: [{path: 'b', outlet: 'b', component: ComponentB}], + }, + {path: 'c', outlet: 'c', component: ComponentC}, + ], + '(b:b//c:c)', + (t: UrlTree) => { + expect(t.toString()).toEqual('/(b:b//c:c)'); + }, + ); }); - it('should work with children outlets within two levels of empty parents', () => { checkRedirect( - [{ + [ + { path: '', component: ComponentA, - children: [{ - path: '', - component: ComponentB, - children: [ - {path: 'd', outlet: 'aux', redirectTo: 'c'}, - {path: 'c', outlet: 'aux', component: ComponentC} - ] - }] - }], - '(aux:d)', (t: UrlTree) => { - expect(t.toString()).toEqual('/(aux:c)'); - }); + children: [ + { + path: '', + component: ComponentB, + children: [ + {path: 'd', outlet: 'aux', redirectTo: 'c'}, + {path: 'c', outlet: 'aux', component: ComponentC}, + ], + }, + ], + }, + ], + '(aux:d)', + (t: UrlTree) => { + expect(t.toString()).toEqual('/(aux:c)'); + }, + ); }); it('does not persist a primary segment beyond the boundary of a named outlet match', () => { @@ -971,111 +1119,141 @@ describe('redirects', () => { path: '', component: ComponentA, outlet: 'aux', - children: [{path: 'b', component: ComponentB, redirectTo: '/c'}] + children: [{path: 'b', component: ComponentB, redirectTo: '/c'}], }, - {path: 'c', component: ComponentC} + {path: 'c', component: ComponentC}, ]; - recognize(testModule.injector, null!, null, config, tree('/b'), serializer) - .subscribe( - (_) => { - throw 'Should not be reached'; - }, - e => { - expect(e.message).toContain(`Cannot match any routes. URL Segment: 'b'`); - }); + recognize(testModule.injector, null!, null, config, tree('/b'), serializer).subscribe( + (_) => { + throw 'Should not be reached'; + }, + (e) => { + expect(e.message).toContain(`Cannot match any routes. URL Segment: 'b'`); + }, + ); }); }); describe('split at the end (no right child)', () => { it('should create a new child (non-terminal)', () => { checkRedirect( - [{ + [ + { path: 'a', children: [ - {path: 'b', component: ComponentB}, {path: '', redirectTo: 'b'}, + {path: 'b', component: ComponentB}, + {path: '', redirectTo: 'b'}, {path: 'c', component: ComponentC, outlet: 'aux'}, - {path: '', redirectTo: 'c', outlet: 'aux'} - ] - }], - 'a', (t: UrlTree) => { - expectTreeToBe(t, 'a/(b//aux:c)'); - }); + {path: '', redirectTo: 'c', outlet: 'aux'}, + ], + }, + ], + 'a', + (t: UrlTree) => { + expectTreeToBe(t, 'a/(b//aux:c)'); + }, + ); }); it('should create a new child (terminal)', () => { checkRedirect( - [{ + [ + { path: 'a', children: [ - {path: 'b', component: ComponentB}, {path: '', redirectTo: 'b'}, + {path: 'b', component: ComponentB}, + {path: '', redirectTo: 'b'}, {path: 'c', component: ComponentC, outlet: 'aux'}, - {path: '', pathMatch: 'full', redirectTo: 'c', outlet: 'aux'} - ] - }], - 'a', (t: UrlTree) => { - expectTreeToBe(t, 'a/(b//aux:c)'); - }); + {path: '', pathMatch: 'full', redirectTo: 'c', outlet: 'aux'}, + ], + }, + ], + 'a', + (t: UrlTree) => { + expectTreeToBe(t, 'a/(b//aux:c)'); + }, + ); }); it('should work only only primary outlet', () => { checkRedirect( - [{ + [ + { path: 'a', children: [ - {path: 'b', component: ComponentB}, {path: '', redirectTo: 'b'}, - {path: 'c', component: ComponentC, outlet: 'aux'} - ] - }], - 'a/(aux:c)', (t: UrlTree) => { - expectTreeToBe(t, 'a/(b//aux:c)'); - }); + {path: 'b', component: ComponentB}, + {path: '', redirectTo: 'b'}, + {path: 'c', component: ComponentC, outlet: 'aux'}, + ], + }, + ], + 'a/(aux:c)', + (t: UrlTree) => { + expectTreeToBe(t, 'a/(b//aux:c)'); + }, + ); }); }); describe('split at the end (right child)', () => { it('should create a new child (non-terminal)', () => { checkRedirect( - [{ + [ + { path: 'a', children: [ {path: 'b', component: ComponentB, children: [{path: 'd', component: ComponentB}]}, - {path: '', redirectTo: 'b'}, { + {path: '', redirectTo: 'b'}, + { path: 'c', component: ComponentC, outlet: 'aux', - children: [{path: 'e', component: ComponentC}] + children: [{path: 'e', component: ComponentC}], }, - {path: '', redirectTo: 'c', outlet: 'aux'} - ] - }], - 'a/(d//aux:e)', (t: UrlTree) => { - expectTreeToBe(t, 'a/(b/d//aux:c/e)'); - }); + {path: '', redirectTo: 'c', outlet: 'aux'}, + ], + }, + ], + 'a/(d//aux:e)', + (t: UrlTree) => { + expectTreeToBe(t, 'a/(b/d//aux:c/e)'); + }, + ); }); it('should not create a new child (terminal)', () => { - const config: Routes = [{ - path: 'a', - children: [ - {path: 'b', component: ComponentB, children: [{path: 'd', component: ComponentB}]}, - {path: '', redirectTo: 'b'}, { - path: 'c', - component: ComponentC, - outlet: 'aux', - children: [{path: 'e', component: ComponentC}] - }, - {path: '', pathMatch: 'full', redirectTo: 'c', outlet: 'aux'} - ] - }]; + const config: Routes = [ + { + path: 'a', + children: [ + {path: 'b', component: ComponentB, children: [{path: 'd', component: ComponentB}]}, + {path: '', redirectTo: 'b'}, + { + path: 'c', + component: ComponentC, + outlet: 'aux', + children: [{path: 'e', component: ComponentC}], + }, + {path: '', pathMatch: 'full', redirectTo: 'c', outlet: 'aux'}, + ], + }, + ]; - recognize(testModule.injector, null!, null, config, tree('a/(d//aux:e)'), serializer) - .subscribe( - (_) => { - throw 'Should not be reached'; - }, - e => { - expect(e.message).toContain('Cannot match any routes. URL Segment: \'a\''); - }); + recognize( + testModule.injector, + null!, + null, + config, + tree('a/(d//aux:e)'), + serializer, + ).subscribe( + (_) => { + throw 'Should not be reached'; + }, + (e) => { + expect(e.message).toContain("Cannot match any routes. URL Segment: 'a'"); + }, + ); }); }); }); @@ -1083,40 +1261,50 @@ describe('redirects', () => { describe('empty URL leftovers', () => { it('should not error when no children matching and no url is left', () => { checkRedirect( - [{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}], - '/a', (t: UrlTree) => { - expectTreeToBe(t, 'a'); - }); + [{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}], + '/a', + (t: UrlTree) => { + expectTreeToBe(t, 'a'); + }, + ); }); it('should not error when no children matching and no url is left (aux routes)', () => { checkRedirect( - [{ + [ + { path: 'a', component: ComponentA, children: [ {path: 'b', component: ComponentB}, {path: '', redirectTo: 'c', outlet: 'aux'}, {path: 'c', component: ComponentC, outlet: 'aux'}, - ] - }], - '/a', (t: UrlTree) => { - expectTreeToBe(t, 'a/(aux:c)'); - }); + ], + }, + ], + '/a', + (t: UrlTree) => { + expectTreeToBe(t, 'a/(aux:c)'); + }, + ); }); it('should error when no children matching and some url is left', () => { recognize( - testModule.injector, null!, null, - [{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}], - tree('/a/c'), serializer) - .subscribe( - (_) => { - throw 'Should not be reached'; - }, - e => { - expect(e.message).toContain('Cannot match any routes. URL Segment: \'a/c\''); - }); + testModule.injector, + null!, + null, + [{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}], + tree('/a/c'), + serializer, + ).subscribe( + (_) => { + throw 'Should not be reached'; + }, + (e) => { + expect(e.message).toContain("Cannot match any routes. URL Segment: 'a/c'"); + }, + ); }); }); @@ -1131,128 +1319,149 @@ describe('redirects', () => { }; checkRedirect( - [{ + [ + { matcher: matcher, component: ComponentA, - children: [{path: 'b', component: ComponentB}] - }], - '/a/1/b', (t: UrlTree) => { - expectTreeToBe(t, 'a/1/b'); - }); + children: [{path: 'b', component: ComponentB}], + }, + ], + '/a/1/b', + (t: UrlTree) => { + expectTreeToBe(t, 'a/1/b'); + }, + ); }); }); describe('multiple matches with empty path named outlets', () => { it('should work with redirects when other outlet comes before the one being activated', () => { recognize( - testModule.injector, null!, null, - [ - { - path: '', - children: [ - {path: '', outlet: 'aux', redirectTo: 'b'}, - {path: 'b', component: ComponentA, outlet: 'aux'}, - {path: '', redirectTo: 'b', pathMatch: 'full'}, - {path: 'b', component: ComponentB}, - ], - }, - ], - tree(''), serializer) - .subscribe( - ({tree}) => { - expect(tree.toString()).toEqual('/b(aux:b)'); - expect(tree.root.children['primary'].toString()).toEqual('b'); - expect(tree.root.children['aux']).toBeDefined(); - expect(tree.root.children['aux'].toString()).toEqual('b'); - }, - () => { - fail('should not be reached'); - }); + testModule.injector, + null!, + null, + [ + { + path: '', + children: [ + {path: '', outlet: 'aux', redirectTo: 'b'}, + {path: 'b', component: ComponentA, outlet: 'aux'}, + {path: '', redirectTo: 'b', pathMatch: 'full'}, + {path: 'b', component: ComponentB}, + ], + }, + ], + tree(''), + serializer, + ).subscribe( + ({tree}) => { + expect(tree.toString()).toEqual('/b(aux:b)'); + expect(tree.root.children['primary'].toString()).toEqual('b'); + expect(tree.root.children['aux']).toBeDefined(); + expect(tree.root.children['aux'].toString()).toEqual('b'); + }, + () => { + fail('should not be reached'); + }, + ); }); - it('should prevent empty named outlets from appearing in leaves, resulting in odd tree url', - () => { - recognize( - testModule.injector, null!, null, - [ - { - path: '', - children: [ - {path: '', component: ComponentA, outlet: 'aux'}, - {path: '', redirectTo: 'b', pathMatch: 'full'}, - {path: 'b', component: ComponentB}, - ], - }, - ], - tree(''), serializer) - .subscribe( - ({tree}) => { - expect(tree.toString()).toEqual('/b'); - }, - () => { - fail('should not be reached'); - }); - }); - + it('should prevent empty named outlets from appearing in leaves, resulting in odd tree url', () => { + recognize( + testModule.injector, + null!, + null, + [ + { + path: '', + children: [ + {path: '', component: ComponentA, outlet: 'aux'}, + {path: '', redirectTo: 'b', pathMatch: 'full'}, + {path: 'b', component: ComponentB}, + ], + }, + ], + tree(''), + serializer, + ).subscribe( + ({tree}) => { + expect(tree.toString()).toEqual('/b'); + }, + () => { + fail('should not be reached'); + }, + ); + }); it('should work when entry point is named outlet', () => { recognize( - testModule.injector, null!, null, - [ - {path: '', component: ComponentA}, - {path: 'modal', component: ComponentB, outlet: 'popup'}, - ], - tree('(popup:modal)'), serializer) - .subscribe( - ({tree}) => { - expect(tree.toString()).toEqual('/(popup:modal)'); - }, - (e) => { - fail('should not be reached' + e.message); - }); + testModule.injector, + null!, + null, + [ + {path: '', component: ComponentA}, + {path: 'modal', component: ComponentB, outlet: 'popup'}, + ], + tree('(popup:modal)'), + serializer, + ).subscribe( + ({tree}) => { + expect(tree.toString()).toEqual('/(popup:modal)'); + }, + (e) => { + fail('should not be reached' + e.message); + }, + ); }); }); describe('redirecting to named outlets', () => { it('should work when using absolute redirects', () => { checkRedirect( - [ - {path: 'a/:id', redirectTo: '/b/:id(aux:c/:id)'}, - {path: 'b/:id', component: ComponentB}, - {path: 'c/:id', component: ComponentC, outlet: 'aux'} - ], - 'a/1;p=99', (t: UrlTree) => { - expectTreeToBe(t, '/b/1;p=99(aux:c/1;p=99)'); - }); + [ + {path: 'a/:id', redirectTo: '/b/:id(aux:c/:id)'}, + {path: 'b/:id', component: ComponentB}, + {path: 'c/:id', component: ComponentC, outlet: 'aux'}, + ], + 'a/1;p=99', + (t: UrlTree) => { + expectTreeToBe(t, '/b/1;p=99(aux:c/1;p=99)'); + }, + ); }); it('should work when using absolute redirects (wildcard)', () => { checkRedirect( - [ - {path: 'b', component: ComponentB}, - {path: 'c', component: ComponentC, outlet: 'aux'}, - {path: '**', redirectTo: '/b(aux:c)'}, - ], - 'a/1', (t: UrlTree) => { - expectTreeToBe(t, '/b(aux:c)'); - }); + [ + {path: 'b', component: ComponentB}, + {path: 'c', component: ComponentC, outlet: 'aux'}, + {path: '**', redirectTo: '/b(aux:c)'}, + ], + 'a/1', + (t: UrlTree) => { + expectTreeToBe(t, '/b(aux:c)'); + }, + ); }); it('should throw when using non-absolute redirects', () => { recognize( - testModule.injector, null!, null, - [ - {path: 'a', redirectTo: 'b(aux:c)'}, - ], - tree('a'), serializer) - .subscribe( - () => { - throw new Error('should not be reached'); - }, - (e) => { - expect(e.message).toContain( - 'Only absolute redirects can have named outlets. redirectTo: \'b(aux:c)\''); - }); + testModule.injector, + null!, + null, + [{path: 'a', redirectTo: 'b(aux:c)'}], + tree('a'), + serializer, + ).subscribe( + () => { + throw new Error('should not be reached'); + }, + (e) => { + expect(e.message).toContain( + "Only absolute redirects can have named outlets. redirectTo: 'b(aux:c)'", + ); + }, + ); }); }); @@ -1263,25 +1472,30 @@ describe('redirects', () => { config.push({path: 'no_match', component: ComponentB}); } config.push({path: 'match', component: ComponentA}); - recognize(testModule.injector, null!, null, config, tree('match'), serializer).forEach(({ - tree: r - }) => { - expectTreeToBe(r, 'match'); - }); + recognize(testModule.injector, null!, null, config, tree('match'), serializer).forEach( + ({tree: r}) => { + expectTreeToBe(r, 'match'); + }, + ); }); }); function checkRedirect(config: Routes, url: string, callback: any): void { recognize( - TestBed.inject(EnvironmentInjector), TestBed.inject(RouterConfigLoader), null, config, - tree(url), new DefaultUrlSerializer()) - .pipe(map(result => result.tree)) - .subscribe({ - next: callback, - error: e => { - throw e; - } - }); + TestBed.inject(EnvironmentInjector), + TestBed.inject(RouterConfigLoader), + null, + config, + tree(url), + new DefaultUrlSerializer(), + ) + .pipe(map((result) => result.tree)) + .subscribe({ + next: callback, + error: (e) => { + throw e; + }, + }); } function tree(url: string): UrlTree { @@ -1291,8 +1505,9 @@ function tree(url: string): UrlTree { function expectTreeToBe(actual: UrlTree, expectedUrl: string): void { const expected = tree(expectedUrl); const serializer = new DefaultUrlSerializer(); - const error = - `"${serializer.serialize(actual)}" is not equal to "${serializer.serialize(expected)}"`; + const error = `"${serializer.serialize(actual)}" is not equal to "${serializer.serialize( + expected, + )}"`; compareSegments(actual.root, expected.root, error); expect(actual.queryParams).toEqual(expected.queryParams); expect(actual.fragment).toEqual(expected.fragment); @@ -1304,7 +1519,7 @@ function compareSegments(actual: UrlSegmentGroup, expected: UrlSegmentGroup, err expect(Object.keys(actual.children).length).toEqual(Object.keys(expected.children).length, error); - Object.keys(expected.children).forEach(key => { + Object.keys(expected.children).forEach((key) => { compareSegments(actual.children[key], expected.children[key], error); }); } diff --git a/packages/router/test/bootstrap.spec.ts b/packages/router/test/bootstrap.spec.ts index 91890118593..ca5957f153f 100644 --- a/packages/router/test/bootstrap.spec.ts +++ b/packages/router/test/bootstrap.spec.ts @@ -10,11 +10,28 @@ import {DOCUMENT, PlatformLocation, ɵgetDOM as getDOM} from '@angular/common'; import {BrowserPlatformLocation} from '@angular/common/src/location/platform_location'; import {NullViewportScroller, ViewportScroller} from '@angular/common/src/viewport_scroller'; import {MockPlatformLocation} from '@angular/common/testing'; -import {ApplicationRef, Component, CUSTOM_ELEMENTS_SCHEMA, destroyPlatform, ENVIRONMENT_INITIALIZER, inject, Injectable, NgModule} from '@angular/core'; +import { + ApplicationRef, + Component, + CUSTOM_ELEMENTS_SCHEMA, + destroyPlatform, + ENVIRONMENT_INITIALIZER, + inject, + Injectable, + NgModule, +} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; -import {Event, NavigationEnd, provideRouter, Router, RouterModule, RouterOutlet, withEnabledBlockingInitialNavigation} from '@angular/router'; +import { + Event, + NavigationEnd, + provideRouter, + Router, + RouterModule, + RouterOutlet, + withEnabledBlockingInitialNavigation, +} from '@angular/router'; // This is needed, because all files under `packages/` are compiled together as part of the // [legacy-unit-tests-saucelabs][1] CI job, including the `lib.webworker.d.ts` typings brought in by @@ -31,8 +48,7 @@ describe('bootstrap', () => { let testProviders: any[] = null!; @Component({template: 'simple'}) - class SimpleCmp { - } + class SimpleCmp {} @Component({selector: 'test-app', template: 'root '}) class RootCmp { @@ -42,14 +58,13 @@ describe('bootstrap', () => { } @Component({selector: 'test-app2', template: 'root '}) - class SecondRootCmp { - } + class SecondRootCmp {} @Injectable({providedIn: 'root'}) class TestResolver { resolve() { let resolve: (value: unknown) => void; - const res = new Promise(r => resolve = r); + const res = new Promise((r) => (resolve = r)); setTimeout(() => resolve('test-data'), 0); return res; } @@ -76,83 +91,88 @@ describe('bootstrap', () => { {provide: DOCUMENT, useValue: doc}, {provide: ViewportScroller, useClass: isNode ? NullViewportScroller : ViewportScroller}, {provide: PlatformLocation, useClass: MockPlatformLocation}, - provideNavigationEndAction(resolveFn) + provideNavigationEndAction(resolveFn), ]; }); afterEach(destroyPlatform); - it('should complete when initial navigation fails and initialNavigation = enabledBlocking', - async () => { - @NgModule({ - imports: [BrowserModule], - declarations: [RootCmp], - bootstrap: [RootCmp], - providers: [ - ...testProviders, - provideRouter( - [{ - matcher: () => { - throw new Error('error in matcher'); - }, - children: [] - }], - withEnabledBlockingInitialNavigation()) - ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }) - class TestModule { - constructor(router: Router) { - log.push('TestModule'); - router.events.subscribe(e => log.push(e.constructor.name)); - } - } + it('should complete when initial navigation fails and initialNavigation = enabledBlocking', async () => { + @NgModule({ + imports: [BrowserModule], + declarations: [RootCmp], + bootstrap: [RootCmp], + providers: [ + ...testProviders, + provideRouter( + [ + { + matcher: () => { + throw new Error('error in matcher'); + }, + children: [], + }, + ], + withEnabledBlockingInitialNavigation(), + ), + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + class TestModule { + constructor(router: Router) { + log.push('TestModule'); + router.events.subscribe((e) => log.push(e.constructor.name)); + } + } - await platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { - const router = res.injector.get(Router); - expect(router.navigated).toEqual(false); - expect(router.getCurrentNavigation()).toBeNull(); - expect(log).toContain('TestModule'); - expect(log).toContain('NavigationError'); - }); - }); + await platformBrowserDynamic([]) + .bootstrapModule(TestModule) + .then((res) => { + const router = res.injector.get(Router); + expect(router.navigated).toEqual(false); + expect(router.getCurrentNavigation()).toBeNull(); + expect(log).toContain('TestModule'); + expect(log).toContain('NavigationError'); + }); + }); - it('should finish navigation when initial navigation is enabledBlocking and component renavigates on render', - async () => { - @Component({ - template: '', - standalone: true, - }) - class Renavigate { - constructor(router: Router) { - router.navigateByUrl('/other'); - } - } - @Component({ - template: '', - standalone: true, - }) - class BlankCmp { - } + it('should finish navigation when initial navigation is enabledBlocking and component renavigates on render', async () => { + @Component({ + template: '', + standalone: true, + }) + class Renavigate { + constructor(router: Router) { + router.navigateByUrl('/other'); + } + } + @Component({ + template: '', + standalone: true, + }) + class BlankCmp {} - @NgModule({ - imports: [BrowserModule, RouterOutlet], - declarations: [RootCmp], - bootstrap: [RootCmp], - providers: [ - ...testProviders, - provideRouter( - [{path: '', component: Renavigate}, {path: 'other', component: BlankCmp}], - withEnabledBlockingInitialNavigation()) - ], - }) - class TestModule { - } + @NgModule({ + imports: [BrowserModule, RouterOutlet], + declarations: [RootCmp], + bootstrap: [RootCmp], + providers: [ + ...testProviders, + provideRouter( + [ + {path: '', component: Renavigate}, + {path: 'other', component: BlankCmp}, + ], + withEnabledBlockingInitialNavigation(), + ), + ], + }) + class TestModule {} - await expectAsync(Promise.all([ - platformBrowserDynamic([]).bootstrapModule(TestModule), navigationEndPromise - ])).toBeResolved(); - }); + await expectAsync( + Promise.all([platformBrowserDynamic([]).bootstrapModule(TestModule), navigationEndPromise]), + ).toBeResolved(); + }); it('should wait for redirect when initialNavigation = enabledBlocking', async () => { @Injectable({providedIn: 'root'}) @@ -167,16 +187,17 @@ describe('bootstrap', () => { imports: [ BrowserModule, RouterModule.forRoot( - [ - {path: 'redirectToMe', children: [], resolve: {test: TestResolver}}, - {path: '**', canActivate: [Redirect], children: []} - ], - {initialNavigation: 'enabledBlocking'}) + [ + {path: 'redirectToMe', children: [], resolve: {test: TestResolver}}, + {path: '**', canActivate: [Redirect], children: []}, + ], + {initialNavigation: 'enabledBlocking'}, + ), ], declarations: [RootCmp], bootstrap: [RootCmp], providers: [...testProviders], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) class TestModule { constructor() { @@ -184,12 +205,14 @@ describe('bootstrap', () => { } } - const bootstrapPromise = platformBrowserDynamic([]).bootstrapModule(TestModule).then((ref) => { - const router = ref.injector.get(Router); - expect(router.navigated).toEqual(true); - expect(router.url).toContain('redirectToMe'); - expect(log).toContain('TestModule'); - }); + const bootstrapPromise = platformBrowserDynamic([]) + .bootstrapModule(TestModule) + .then((ref) => { + const router = ref.injector.get(Router); + expect(router.navigated).toEqual(true); + expect(router.url).toContain('redirectToMe'); + expect(log).toContain('TestModule'); + }); await Promise.all([bootstrapPromise, navigationEndPromise]); }); @@ -199,19 +222,21 @@ describe('bootstrap', () => { imports: [ BrowserModule, RouterModule.forRoot( - [ - {path: 'redirectToMe', children: []}, { - path: '**', - canActivate: [() => inject(Router).createUrlTree(['redirectToMe'])], - children: [] - } - ], - {initialNavigation: 'enabledBlocking'}) + [ + {path: 'redirectToMe', children: []}, + { + path: '**', + canActivate: [() => inject(Router).createUrlTree(['redirectToMe'])], + children: [], + }, + ], + {initialNavigation: 'enabledBlocking'}, + ), ], declarations: [RootCmp], bootstrap: [RootCmp], providers: [...testProviders], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) class TestModule { constructor() { @@ -219,211 +244,227 @@ describe('bootstrap', () => { } } - const bootstrapPromise = platformBrowserDynamic([]).bootstrapModule(TestModule).then((ref) => { - const router = ref.injector.get(Router); - expect(router.navigated).toEqual(true); - expect(router.url).toContain('redirectToMe'); - expect(log).toContain('TestModule'); - }); + const bootstrapPromise = platformBrowserDynamic([]) + .bootstrapModule(TestModule) + .then((ref) => { + const router = ref.injector.get(Router); + expect(router.navigated).toEqual(true); + expect(router.url).toContain('redirectToMe'); + expect(log).toContain('TestModule'); + }); await Promise.all([bootstrapPromise, navigationEndPromise]); }); it('should wait for resolvers to complete when initialNavigation = enabledBlocking', async () => { @Component({selector: 'test', template: 'test'}) - class TestCmpEnabled { - } + class TestCmpEnabled {} @NgModule({ imports: [ BrowserModule, RouterModule.forRoot( - [{path: '**', component: TestCmpEnabled, resolve: {test: TestResolver}}], - {initialNavigation: 'enabledBlocking'}) + [{path: '**', component: TestCmpEnabled, resolve: {test: TestResolver}}], + {initialNavigation: 'enabledBlocking'}, + ), ], declarations: [RootCmp, TestCmpEnabled], bootstrap: [RootCmp], providers: [...testProviders, TestResolver], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) class TestModule { constructor(router: Router) {} } - const bootstrapPromise = platformBrowserDynamic([]).bootstrapModule(TestModule).then((ref) => { - const router = ref.injector.get(Router); - const data = router.routerState.snapshot.root.firstChild!.data; - expect(data['test']).toEqual('test-data'); - // Also ensure that the navigation completed. The navigation transition clears the - // current navigation in its `finalize` operator. - expect(router.getCurrentNavigation()).toBeNull(); - }); + const bootstrapPromise = platformBrowserDynamic([]) + .bootstrapModule(TestModule) + .then((ref) => { + const router = ref.injector.get(Router); + const data = router.routerState.snapshot.root.firstChild!.data; + expect(data['test']).toEqual('test-data'); + // Also ensure that the navigation completed. The navigation transition clears the + // current navigation in its `finalize` operator. + expect(router.getCurrentNavigation()).toBeNull(); + }); await Promise.all([bootstrapPromise, navigationEndPromise]); }); - it('should NOT wait for resolvers to complete when initialNavigation = enabledNonBlocking', - async () => { - @Component({selector: 'test', template: 'test'}) - class TestCmpLegacyEnabled { - } - - @NgModule({ - imports: [ - BrowserModule, - RouterModule.forRoot( - [{path: '**', component: TestCmpLegacyEnabled, resolve: {test: TestResolver}}], - {initialNavigation: 'enabledNonBlocking'}) - ], - declarations: [RootCmp, TestCmpLegacyEnabled], - bootstrap: [RootCmp], - providers: [...testProviders, TestResolver], - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }) - class TestModule { - constructor(router: Router) { - log.push('TestModule'); - router.events.subscribe(e => log.push(e.constructor.name)); - } - } - - const bootstrapPromise = - platformBrowserDynamic([]).bootstrapModule(TestModule).then((ref) => { - const router: Router = ref.injector.get(Router); - expect(router.routerState.snapshot.root.firstChild).toBeNull(); - // ResolveEnd has not been emitted yet because bootstrap returned too early - expect(log).toEqual([ - 'TestModule', 'RootCmp', 'NavigationStart', 'RoutesRecognized', 'GuardsCheckStart', - 'ChildActivationStart', 'ActivationStart', 'GuardsCheckEnd', 'ResolveStart' - ]); - }); - - await Promise.all([bootstrapPromise, navigationEndPromise]); - }); - - it('should NOT wait for resolvers to complete when initialNavigation is not set', async () => { + it('should NOT wait for resolvers to complete when initialNavigation = enabledNonBlocking', async () => { @Component({selector: 'test', template: 'test'}) - class TestCmpLegacyEnabled { - } + class TestCmpLegacyEnabled {} @NgModule({ imports: [ BrowserModule, RouterModule.forRoot( - [{path: '**', component: TestCmpLegacyEnabled, resolve: {test: TestResolver}}], - ) + [{path: '**', component: TestCmpLegacyEnabled, resolve: {test: TestResolver}}], + {initialNavigation: 'enabledNonBlocking'}, + ), ], declarations: [RootCmp, TestCmpLegacyEnabled], bootstrap: [RootCmp], providers: [...testProviders, TestResolver], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) class TestModule { constructor(router: Router) { log.push('TestModule'); - router.events.subscribe(e => log.push(e.constructor.name)); + router.events.subscribe((e) => log.push(e.constructor.name)); } } - const bootstrapPromise = platformBrowserDynamic([]).bootstrapModule(TestModule).then(ref => { - const router: Router = ref.injector.get(Router); - expect(router.routerState.snapshot.root.firstChild).toBeNull(); - // ResolveEnd has not been emitted yet because bootstrap returned too early - expect(log).toEqual([ - 'TestModule', 'RootCmp', 'NavigationStart', 'RoutesRecognized', 'GuardsCheckStart', - 'ChildActivationStart', 'ActivationStart', 'GuardsCheckEnd', 'ResolveStart' - ]); - }); + const bootstrapPromise = platformBrowserDynamic([]) + .bootstrapModule(TestModule) + .then((ref) => { + const router: Router = ref.injector.get(Router); + expect(router.routerState.snapshot.root.firstChild).toBeNull(); + // ResolveEnd has not been emitted yet because bootstrap returned too early + expect(log).toEqual([ + 'TestModule', + 'RootCmp', + 'NavigationStart', + 'RoutesRecognized', + 'GuardsCheckStart', + 'ChildActivationStart', + 'ActivationStart', + 'GuardsCheckEnd', + 'ResolveStart', + ]); + }); + + await Promise.all([bootstrapPromise, navigationEndPromise]); + }); + + it('should NOT wait for resolvers to complete when initialNavigation is not set', async () => { + @Component({selector: 'test', template: 'test'}) + class TestCmpLegacyEnabled {} + + @NgModule({ + imports: [ + BrowserModule, + RouterModule.forRoot([ + {path: '**', component: TestCmpLegacyEnabled, resolve: {test: TestResolver}}, + ]), + ], + declarations: [RootCmp, TestCmpLegacyEnabled], + bootstrap: [RootCmp], + providers: [...testProviders, TestResolver], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + class TestModule { + constructor(router: Router) { + log.push('TestModule'); + router.events.subscribe((e) => log.push(e.constructor.name)); + } + } + + const bootstrapPromise = platformBrowserDynamic([]) + .bootstrapModule(TestModule) + .then((ref) => { + const router: Router = ref.injector.get(Router); + expect(router.routerState.snapshot.root.firstChild).toBeNull(); + // ResolveEnd has not been emitted yet because bootstrap returned too early + expect(log).toEqual([ + 'TestModule', + 'RootCmp', + 'NavigationStart', + 'RoutesRecognized', + 'GuardsCheckStart', + 'ChildActivationStart', + 'ActivationStart', + 'GuardsCheckEnd', + 'ResolveStart', + ]); + }); await Promise.all([bootstrapPromise, navigationEndPromise]); }); it('should not run navigation when initialNavigation = disabled', (done) => { @Component({selector: 'test', template: 'test'}) - class TestCmpDiabled { - } + class TestCmpDiabled {} @NgModule({ imports: [ BrowserModule, RouterModule.forRoot( - [{path: '**', component: TestCmpDiabled, resolve: {test: TestResolver}}], - {initialNavigation: 'disabled'}) + [{path: '**', component: TestCmpDiabled, resolve: {test: TestResolver}}], + {initialNavigation: 'disabled'}, + ), ], declarations: [RootCmp, TestCmpDiabled], bootstrap: [RootCmp], providers: [...testProviders, TestResolver], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) class TestModule { constructor(router: Router) { log.push('TestModule'); - router.events.subscribe(e => log.push(e.constructor.name)); + router.events.subscribe((e) => log.push(e.constructor.name)); } } - platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { - const router = res.injector.get(Router); - expect(log).toEqual(['TestModule', 'RootCmp']); - done(); - }); + platformBrowserDynamic([]) + .bootstrapModule(TestModule) + .then((res) => { + const router = res.injector.get(Router); + expect(log).toEqual(['TestModule', 'RootCmp']); + done(); + }); }); - it('should not init router navigation listeners if a non root component is bootstrapped', - async () => { - @NgModule({ - imports: [ - BrowserModule, - RouterModule.forRoot( - [], - ) - ], - declarations: [SecondRootCmp, RootCmp], - bootstrap: [RootCmp], - providers: testProviders, - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }) - class TestModule { - } + it('should not init router navigation listeners if a non root component is bootstrapped', async () => { + @NgModule({ + imports: [BrowserModule, RouterModule.forRoot([])], + declarations: [SecondRootCmp, RootCmp], + bootstrap: [RootCmp], + providers: testProviders, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + class TestModule {} - await platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { - const router = res.injector.get(Router); - spyOn(router as any, 'resetRootComponentType').and.callThrough(); + await platformBrowserDynamic([]) + .bootstrapModule(TestModule) + .then((res) => { + const router = res.injector.get(Router); + spyOn(router as any, 'resetRootComponentType').and.callThrough(); - const appRef: ApplicationRef = res.injector.get(ApplicationRef); - appRef.bootstrap(SecondRootCmp); + const appRef: ApplicationRef = res.injector.get(ApplicationRef); + appRef.bootstrap(SecondRootCmp); - expect((router as any).resetRootComponentType).not.toHaveBeenCalled(); - }); - }); + expect((router as any).resetRootComponentType).not.toHaveBeenCalled(); + }); + }); - it('should reinit router navigation listeners if a previously bootstrapped root component is destroyed', - async () => { - @NgModule({ - imports: [BrowserModule, RouterModule.forRoot([])], - declarations: [SecondRootCmp, RootCmp], - bootstrap: [RootCmp], - providers: testProviders, - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }) - class TestModule { - } + it('should reinit router navigation listeners if a previously bootstrapped root component is destroyed', async () => { + @NgModule({ + imports: [BrowserModule, RouterModule.forRoot([])], + declarations: [SecondRootCmp, RootCmp], + bootstrap: [RootCmp], + providers: testProviders, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + class TestModule {} - await platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { - const router = res.injector.get(Router); - spyOn(router as any, 'resetRootComponentType').and.callThrough(); + await platformBrowserDynamic([]) + .bootstrapModule(TestModule) + .then((res) => { + const router = res.injector.get(Router); + spyOn(router as any, 'resetRootComponentType').and.callThrough(); - const appRef: ApplicationRef = res.injector.get(ApplicationRef); - const {promise, resolveFn} = createPromise(); - appRef.components[0].onDestroy(() => { - appRef.bootstrap(SecondRootCmp); - expect((router as any).resetRootComponentType).toHaveBeenCalled(); - resolveFn(); - }); + const appRef: ApplicationRef = res.injector.get(ApplicationRef); + const {promise, resolveFn} = createPromise(); + appRef.components[0].onDestroy(() => { + appRef.bootstrap(SecondRootCmp); + expect((router as any).resetRootComponentType).toHaveBeenCalled(); + resolveFn(); + }); - appRef.components[0].destroy(); - return promise; - }); - }); + appRef.components[0].destroy(); + return promise; + }); + }); if (!isNode) { it('should restore the scrolling position', async () => { @@ -437,34 +478,34 @@ describe('bootstrap', () => {
- ` + `, }) - class TallComponent { - } + class TallComponent {} @NgModule({ imports: [ BrowserModule, RouterModule.forRoot( - [ - {path: '', pathMatch: 'full', redirectTo: '/aa'}, - {path: 'aa', component: TallComponent}, {path: 'bb', component: TallComponent}, - {path: 'cc', component: TallComponent}, - {path: 'fail', component: TallComponent, canActivate: [() => false]} - ], - { - scrollPositionRestoration: 'enabled', - anchorScrolling: 'enabled', - scrollOffset: [0, 100], - onSameUrlNavigation: 'ignore', - }) + [ + {path: '', pathMatch: 'full', redirectTo: '/aa'}, + {path: 'aa', component: TallComponent}, + {path: 'bb', component: TallComponent}, + {path: 'cc', component: TallComponent}, + {path: 'fail', component: TallComponent, canActivate: [() => false]}, + ], + { + scrollPositionRestoration: 'enabled', + anchorScrolling: 'enabled', + scrollOffset: [0, 100], + onSameUrlNavigation: 'ignore', + }, + ), ], declarations: [TallComponent, RootCmp], bootstrap: [RootCmp], providers: [...testProviders], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) - class TestModule { - } + class TestModule {} function resolveAfter(milliseconds: number) { return new Promise((resolve) => { @@ -495,7 +536,7 @@ describe('bootstrap', () => { await router.navigateByUrl('/aa#marker2'); await resolveAfter(100); expect(window.scrollY).toBeGreaterThanOrEqual(5900); - expect(window.scrollY).toBeLessThan(6000); // offset + expect(window.scrollY).toBeLessThan(6000); // offset // Scroll somewhere else, then navigate to the hash again. Even though the same url navigation // is ignored by the Router, we should still scroll. @@ -503,7 +544,7 @@ describe('bootstrap', () => { await router.navigateByUrl('/aa#marker2'); await resolveAfter(100); expect(window.scrollY).toBeGreaterThanOrEqual(5900); - expect(window.scrollY).toBeLessThan(6000); // offset + expect(window.scrollY).toBeLessThan(6000); // offset }); it('should cleanup "popstate" and "hashchange" listeners', async () => { @@ -511,11 +552,12 @@ describe('bootstrap', () => { imports: [BrowserModule, RouterModule.forRoot([])], declarations: [RootCmp], bootstrap: [RootCmp], - providers: - [...testProviders, {provide: PlatformLocation, useClass: BrowserPlatformLocation}], + providers: [ + ...testProviders, + {provide: PlatformLocation, useClass: BrowserPlatformLocation}, + ], }) - class TestModule { - } + class TestModule {} spyOn(window, 'addEventListener').and.callThrough(); spyOn(window, 'removeEventListener').and.callThrough(); @@ -525,10 +567,16 @@ describe('bootstrap', () => { expect(window.addEventListener).toHaveBeenCalledTimes(2); - expect(window.addEventListener) - .toHaveBeenCalledWith('popstate', jasmine.any(Function), jasmine.any(Boolean)); - expect(window.addEventListener) - .toHaveBeenCalledWith('hashchange', jasmine.any(Function), jasmine.any(Boolean)); + expect(window.addEventListener).toHaveBeenCalledWith( + 'popstate', + jasmine.any(Function), + jasmine.any(Boolean), + ); + expect(window.addEventListener).toHaveBeenCalledWith( + 'hashchange', + jasmine.any(Function), + jasmine.any(Boolean), + ); expect(window.removeEventListener).toHaveBeenCalledWith('popstate', jasmine.any(Function)); expect(window.removeEventListener).toHaveBeenCalledWith('hashchange', jasmine.any(Function)); @@ -539,19 +587,16 @@ describe('bootstrap', () => { @NgModule({ imports: [ BrowserModule, - RouterModule.forRoot( - [ - {path: 'a', component: SimpleCmp}, - {path: 'b', component: SimpleCmp}, - ], - ) + RouterModule.forRoot([ + {path: 'a', component: SimpleCmp}, + {path: 'b', component: SimpleCmp}, + ]), ], declarations: [RootCmp, SimpleCmp], bootstrap: [RootCmp], providers: [...testProviders], }) - class TestModule { - } + class TestModule {} (async () => { const res = await platformBrowserDynamic([]).bootstrapModule(TestModule); @@ -568,7 +613,7 @@ describe('bootstrap', () => { }); function onNavigationEnd(router: Router, fn: Function) { - router.events.subscribe(e => { + router.events.subscribe((e) => { if (e instanceof NavigationEnd) { fn(); } @@ -581,13 +626,13 @@ function provideNavigationEndAction(fn: Function) { multi: true, useValue: () => { onNavigationEnd(inject(Router), fn); - } + }, }; } function createPromise() { let resolveFn: () => void; - const promise = new Promise(r => { + const promise = new Promise((r) => { resolveFn = r; }); return {resolveFn: () => resolveFn(), promise}; diff --git a/packages/router/test/computed_state_restoration.spec.ts b/packages/router/test/computed_state_restoration.spec.ts index 63979a472e8..f11e11537db 100644 --- a/packages/router/test/computed_state_restoration.spec.ts +++ b/packages/router/test/computed_state_restoration.spec.ts @@ -43,11 +43,11 @@ describe('`restoredState#ɵrouterPageId`', () => { @Injectable({providedIn: 'root'}) class MyCanActivateGuard { allow: boolean = true; - redirectTo: string|null|UrlTree = null; + redirectTo: string | null | UrlTree = null; constructor(private router: Router) {} - canActivate(): boolean|UrlTree { + canActivate(): boolean | UrlTree { if (typeof this.redirectTo === 'string') { this.router.navigateByUrl(this.redirectTo); } else if (isUrlTree(this.redirectTo)) { @@ -66,55 +66,56 @@ describe('`restoredState#ɵrouterPageId`', () => { let fixture: ComponentFixture; - function createNavigationHistory(urlUpdateStrategy: 'eager'|'deferred' = 'deferred') { + function createNavigationHistory(urlUpdateStrategy: 'eager' | 'deferred' = 'deferred') { TestBed.configureTestingModule({ imports: [TestModule], providers: [ {provide: 'alwaysFalse', useValue: (a: any) => false}, {provide: Location, useClass: SpyLocation}, provideRouter( - [ - { - path: 'first', - component: SimpleCmp, - canDeactivate: [MyCanDeactivateGuard], - canActivate: [MyCanActivateGuard, ThrowingCanActivateGuard], - resolve: {x: MyResolve} - }, - { - path: 'second', - component: SimpleCmp, - canDeactivate: [MyCanDeactivateGuard], - canActivate: [MyCanActivateGuard, ThrowingCanActivateGuard], - resolve: {x: MyResolve} - }, - { - path: 'third', - component: SimpleCmp, - canDeactivate: [MyCanDeactivateGuard], - canActivate: [MyCanActivateGuard, ThrowingCanActivateGuard], - resolve: {x: MyResolve} - }, - { - path: 'unguarded', - component: SimpleCmp, - }, - { - path: 'throwing', - component: ThrowingCmp, - }, - { - path: 'loaded', - loadChildren: () => of(ModuleWithSimpleCmpAsRoute), - canLoad: ['alwaysFalse'] - } - ], - withRouterConfig({ - urlUpdateStrategy, - canceledNavigationResolution: 'computed', - resolveNavigationPromiseOnError: true, - })), - ] + [ + { + path: 'first', + component: SimpleCmp, + canDeactivate: [MyCanDeactivateGuard], + canActivate: [MyCanActivateGuard, ThrowingCanActivateGuard], + resolve: {x: MyResolve}, + }, + { + path: 'second', + component: SimpleCmp, + canDeactivate: [MyCanDeactivateGuard], + canActivate: [MyCanActivateGuard, ThrowingCanActivateGuard], + resolve: {x: MyResolve}, + }, + { + path: 'third', + component: SimpleCmp, + canDeactivate: [MyCanDeactivateGuard], + canActivate: [MyCanActivateGuard, ThrowingCanActivateGuard], + resolve: {x: MyResolve}, + }, + { + path: 'unguarded', + component: SimpleCmp, + }, + { + path: 'throwing', + component: ThrowingCmp, + }, + { + path: 'loaded', + loadChildren: () => of(ModuleWithSimpleCmpAsRoute), + canLoad: ['alwaysFalse'], + }, + ], + withRouterConfig({ + urlUpdateStrategy, + canceledNavigationResolution: 'computed', + resolveNavigationPromiseOnError: true, + }), + ), + ], }); const router = TestBed.inject(Router); const location = TestBed.inject(Location); @@ -146,222 +147,216 @@ describe('`restoredState#ɵrouterPageId`', () => { })); it('should work when CanActivate returns false', fakeAsync(() => { - const location = TestBed.inject(Location); - const router = TestBed.inject(Router); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + const location = TestBed.inject(Location); + const router = TestBed.inject(Router); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - TestBed.inject(MyCanActivateGuard).allow = false; - location.back(); - advance(fixture); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + TestBed.inject(MyCanActivateGuard).allow = false; + location.back(); + advance(fixture); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - TestBed.inject(MyCanActivateGuard).allow = true; - location.back(); - advance(fixture); - expect(location.path()).toEqual('/first'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); + TestBed.inject(MyCanActivateGuard).allow = true; + location.back(); + advance(fixture); + expect(location.path()).toEqual('/first'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); - TestBed.inject(MyCanActivateGuard).allow = false; - location.forward(); - advance(fixture); - expect(location.path()).toEqual('/first'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); - - router.navigateByUrl('/second'); - advance(fixture); - expect(location.path()).toEqual('/first'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); - })); + TestBed.inject(MyCanActivateGuard).allow = false; + location.forward(); + advance(fixture); + expect(location.path()).toEqual('/first'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); + router.navigateByUrl('/second'); + advance(fixture); + expect(location.path()).toEqual('/first'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); + })); it('should work when CanDeactivate returns false', fakeAsync(() => { - const location = TestBed.inject(Location); - const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + const router = TestBed.inject(Router); - TestBed.inject(MyCanDeactivateGuard).allow = false; - location.back(); - advance(fixture); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + TestBed.inject(MyCanDeactivateGuard).allow = false; + location.back(); + advance(fixture); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - location.forward(); - advance(fixture); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + location.forward(); + advance(fixture); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - router.navigateByUrl('third'); - advance(fixture); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + router.navigateByUrl('third'); + advance(fixture); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - - TestBed.inject(MyCanDeactivateGuard).allow = true; - location.forward(); - advance(fixture); - expect(location.path()).toEqual('/third'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3})); - })); + TestBed.inject(MyCanDeactivateGuard).allow = true; + location.forward(); + advance(fixture); + expect(location.path()).toEqual('/third'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3})); + })); it('should work when using `NavigationExtras.skipLocationChange`', fakeAsync(() => { - const location = TestBed.inject(Location); - const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + const router = TestBed.inject(Router); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - router.navigateByUrl('/first', {skipLocationChange: true}); - advance(fixture); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + router.navigateByUrl('/first', {skipLocationChange: true}); + advance(fixture); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - router.navigateByUrl('/third'); - advance(fixture); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3})); + router.navigateByUrl('/third'); + advance(fixture); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3})); - location.back(); - advance(fixture); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - })); + location.back(); + advance(fixture); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + })); it('should work when using `NavigationExtras.replaceUrl`', fakeAsync(() => { - const location = TestBed.inject(Location); - const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + const router = TestBed.inject(Router); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - router.navigateByUrl('/first', {replaceUrl: true}); - advance(fixture); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - expect(location.path()).toEqual('/first'); - })); + router.navigateByUrl('/first', {replaceUrl: true}); + advance(fixture); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + expect(location.path()).toEqual('/first'); + })); it('should work when CanLoad returns false', fakeAsync(() => { - const location = TestBed.inject(Location); - const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + const router = TestBed.inject(Router); - router.navigateByUrl('/loaded'); - advance(fixture); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - })); + router.navigateByUrl('/loaded'); + advance(fixture); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + })); it('should work when resolve empty', fakeAsync(() => { - const location = TestBed.inject(Location); - const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + const router = TestBed.inject(Router); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - TestBed.inject(MyResolve).myresolve = EMPTY; + TestBed.inject(MyResolve).myresolve = EMPTY; - location.back(); - advance(fixture); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - expect(location.path()).toEqual('/second'); + location.back(); + advance(fixture); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + expect(location.path()).toEqual('/second'); - TestBed.inject(MyResolve).myresolve = of(2); + TestBed.inject(MyResolve).myresolve = of(2); - location.back(); - advance(fixture); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); - expect(location.path()).toEqual('/first'); + location.back(); + advance(fixture); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); + expect(location.path()).toEqual('/first'); - TestBed.inject(MyResolve).myresolve = EMPTY; + TestBed.inject(MyResolve).myresolve = EMPTY; - // We should cancel the navigation to `/third` when myresolve is empty - router.navigateByUrl('/third'); - advance(fixture); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); - expect(location.path()).toEqual('/first'); + // We should cancel the navigation to `/third` when myresolve is empty + router.navigateByUrl('/third'); + advance(fixture); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); + expect(location.path()).toEqual('/first'); - location.historyGo(2); - advance(fixture); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); - expect(location.path()).toEqual('/first'); + location.historyGo(2); + advance(fixture); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); + expect(location.path()).toEqual('/first'); - TestBed.inject(MyResolve).myresolve = of(2); - location.historyGo(2); - advance(fixture); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3})); - expect(location.path()).toEqual('/third'); - - TestBed.inject(MyResolve).myresolve = EMPTY; - location.historyGo(-2); - advance(fixture); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3})); - expect(location.path()).toEqual('/third'); - })); + TestBed.inject(MyResolve).myresolve = of(2); + location.historyGo(2); + advance(fixture); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3})); + expect(location.path()).toEqual('/third'); + TestBed.inject(MyResolve).myresolve = EMPTY; + location.historyGo(-2); + advance(fixture); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3})); + expect(location.path()).toEqual('/third'); + })); it('should work when an error occurred during navigation', fakeAsync(() => { - const location = TestBed.inject(Location); - const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + const router = TestBed.inject(Router); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + router.navigateByUrl('/invalid').catch(() => null); + advance(fixture); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - router.navigateByUrl('/invalid').catch(() => null); - advance(fixture); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - - location.back(); - advance(fixture); - expect(location.path()).toEqual('/first'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); - })); + location.back(); + advance(fixture); + expect(location.path()).toEqual('/first'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); + })); it('should work when CanActivate redirects', fakeAsync(() => { - const location = TestBed.inject(Location); + const location = TestBed.inject(Location); - TestBed.inject(MyCanActivateGuard).redirectTo = '/unguarded'; - location.back(); - advance(fixture); - expect(location.path()).toEqual('/unguarded'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + TestBed.inject(MyCanActivateGuard).redirectTo = '/unguarded'; + location.back(); + advance(fixture); + expect(location.path()).toEqual('/unguarded'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - TestBed.inject(MyCanActivateGuard).redirectTo = null; + TestBed.inject(MyCanActivateGuard).redirectTo = null; - location.back(); - advance(fixture); - expect(location.path()).toEqual('/first'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); - })); + location.back(); + advance(fixture); + expect(location.path()).toEqual('/first'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); + })); - it('restores history correctly when component throws error in constructor and replaceUrl=true', - fakeAsync(() => { - const location = TestBed.inject(Location); - const router = TestBed.inject(Router); + it('restores history correctly when component throws error in constructor and replaceUrl=true', fakeAsync(() => { + const location = TestBed.inject(Location); + const router = TestBed.inject(Router); - router.navigateByUrl('/throwing', {replaceUrl: true}).catch(() => null); - advance(fixture); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + router.navigateByUrl('/throwing', {replaceUrl: true}).catch(() => null); + advance(fixture); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - location.back(); - advance(fixture); - expect(location.path()).toEqual('/first'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); - })); + location.back(); + advance(fixture); + expect(location.path()).toEqual('/first'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); + })); - it('restores history correctly when component throws error in constructor and skipLocationChange=true', - fakeAsync(() => { - const location = TestBed.inject(Location); - const router = TestBed.inject(Router); + it('restores history correctly when component throws error in constructor and skipLocationChange=true', fakeAsync(() => { + const location = TestBed.inject(Location); + const router = TestBed.inject(Router); - router.navigateByUrl('/throwing', {skipLocationChange: true}).catch(() => null); - advance(fixture); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + router.navigateByUrl('/throwing', {skipLocationChange: true}).catch(() => null); + advance(fixture); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - location.back(); - advance(fixture); - expect(location.path()).toEqual('/first'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); - })); + location.back(); + advance(fixture); + expect(location.path()).toEqual('/first'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); + })); }); describe('eager url updates', () => { @@ -369,147 +364,142 @@ describe('`restoredState#ɵrouterPageId`', () => { createNavigationHistory('eager'); })); it('should work', fakeAsync(() => { - const location = TestBed.inject(Location) as SpyLocation; - const router = TestBed.inject(Router); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + const location = TestBed.inject(Location) as SpyLocation; + const router = TestBed.inject(Router); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - TestBed.inject(MyCanActivateGuard).allow = false; - router.navigateByUrl('/first'); - advance(fixture); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + TestBed.inject(MyCanActivateGuard).allow = false; + router.navigateByUrl('/first'); + advance(fixture); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - location.back(); - advance(fixture); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - })); + location.back(); + advance(fixture); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + })); it('should work when CanActivate redirects', fakeAsync(() => { - const location = TestBed.inject(Location); - const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + const router = TestBed.inject(Router); - TestBed.inject(MyCanActivateGuard).redirectTo = '/unguarded'; - router.navigateByUrl('/third'); - advance(fixture); - expect(location.path()).toEqual('/unguarded'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4})); + TestBed.inject(MyCanActivateGuard).redirectTo = '/unguarded'; + router.navigateByUrl('/third'); + advance(fixture); + expect(location.path()).toEqual('/unguarded'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4})); - TestBed.inject(MyCanActivateGuard).redirectTo = null; + TestBed.inject(MyCanActivateGuard).redirectTo = null; - location.back(); - advance(fixture); - expect(location.path()).toEqual('/third'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3})); - })); + location.back(); + advance(fixture); + expect(location.path()).toEqual('/third'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3})); + })); it('should work when CanActivate redirects with UrlTree', fakeAsync(() => { - // Note that this test is different from the above case because we are able to specifically - // handle the `UrlTree` case as a proper redirect and set `replaceUrl: true` on the - // follow-up navigation. - const location = TestBed.inject(Location); - const router = TestBed.inject(Router); - let allowNavigation = true; - router.resetConfig([ - {path: 'initial', children: []}, - {path: 'redirectFrom', redirectTo: 'redirectTo'}, - {path: 'redirectTo', children: [], canActivate: [() => allowNavigation]}, - ]); + // Note that this test is different from the above case because we are able to specifically + // handle the `UrlTree` case as a proper redirect and set `replaceUrl: true` on the + // follow-up navigation. + const location = TestBed.inject(Location); + const router = TestBed.inject(Router); + let allowNavigation = true; + router.resetConfig([ + {path: 'initial', children: []}, + {path: 'redirectFrom', redirectTo: 'redirectTo'}, + {path: 'redirectTo', children: [], canActivate: [() => allowNavigation]}, + ]); - // already at '2' from the `beforeEach` navigations - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - router.navigateByUrl('/initial'); - advance(fixture); - expect(location.path()).toEqual('/initial'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3})); + // already at '2' from the `beforeEach` navigations + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + router.navigateByUrl('/initial'); + advance(fixture); + expect(location.path()).toEqual('/initial'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3})); - TestBed.inject(MyCanActivateGuard).redirectTo = null; + TestBed.inject(MyCanActivateGuard).redirectTo = null; - router.navigateByUrl('redirectTo'); - advance(fixture); - expect(location.path()).toEqual('/redirectTo'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4})); + router.navigateByUrl('redirectTo'); + advance(fixture); + expect(location.path()).toEqual('/redirectTo'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4})); - // Navigate to different URL but get redirected to same URL should result in same page id - router.navigateByUrl('redirectFrom'); - advance(fixture); - expect(location.path()).toEqual('/redirectTo'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4})); + // Navigate to different URL but get redirected to same URL should result in same page id + router.navigateByUrl('redirectFrom'); + advance(fixture); + expect(location.path()).toEqual('/redirectTo'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4})); - // Back and forward should have page IDs 1 apart - location.back(); - advance(fixture); - expect(location.path()).toEqual('/initial'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3})); - location.forward(); - advance(fixture); - expect(location.path()).toEqual('/redirectTo'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4})); + // Back and forward should have page IDs 1 apart + location.back(); + advance(fixture); + expect(location.path()).toEqual('/initial'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3})); + location.forward(); + advance(fixture); + expect(location.path()).toEqual('/redirectTo'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4})); - // Rejected navigation after redirect to same URL should have the same page ID - allowNavigation = false; - router.navigateByUrl('redirectFrom'); - advance(fixture); - expect(location.path()).toEqual('/redirectTo'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4})); - })); + // Rejected navigation after redirect to same URL should have the same page ID + allowNavigation = false; + router.navigateByUrl('redirectFrom'); + advance(fixture); + expect(location.path()).toEqual('/redirectTo'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4})); + })); it('redirectTo with same url, and guard reject', fakeAsync(() => { - const location = TestBed.inject(Location); - const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + const router = TestBed.inject(Router); - TestBed.inject(MyCanActivateGuard).redirectTo = router.createUrlTree(['unguarded']); - router.navigateByUrl('/third'); - advance(fixture); - expect(location.path()).toEqual('/unguarded'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3})); + TestBed.inject(MyCanActivateGuard).redirectTo = router.createUrlTree(['unguarded']); + router.navigateByUrl('/third'); + advance(fixture); + expect(location.path()).toEqual('/unguarded'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3})); - TestBed.inject(MyCanActivateGuard).redirectTo = null; + TestBed.inject(MyCanActivateGuard).redirectTo = null; - location.back(); - advance(fixture); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - })); + location.back(); + advance(fixture); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + })); }); - for (const urlUpdateStrategy of ['deferred', 'eager'] as const) { - it(`restores history correctly when an error is thrown in guard with urlUpdateStrategy ${ - urlUpdateStrategy}`, - fakeAsync(() => { - createNavigationHistory(urlUpdateStrategy); - const location = TestBed.inject(Location); + it(`restores history correctly when an error is thrown in guard with urlUpdateStrategy ${urlUpdateStrategy}`, fakeAsync(() => { + createNavigationHistory(urlUpdateStrategy); + const location = TestBed.inject(Location); - TestBed.inject(ThrowingCanActivateGuard).throw = true; + TestBed.inject(ThrowingCanActivateGuard).throw = true; - location.back(); - advance(fixture); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + location.back(); + advance(fixture); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - TestBed.inject(ThrowingCanActivateGuard).throw = false; - location.back(); - advance(fixture); - expect(location.path()).toEqual('/first'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); - })); + TestBed.inject(ThrowingCanActivateGuard).throw = false; + location.back(); + advance(fixture); + expect(location.path()).toEqual('/first'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); + })); - it(`restores history correctly when component throws error in constructor with urlUpdateStrategy ${ - urlUpdateStrategy}`, - fakeAsync(() => { - createNavigationHistory(urlUpdateStrategy); - const location = TestBed.inject(Location); - const router = TestBed.inject(Router); + it(`restores history correctly when component throws error in constructor with urlUpdateStrategy ${urlUpdateStrategy}`, fakeAsync(() => { + createNavigationHistory(urlUpdateStrategy); + const location = TestBed.inject(Location); + const router = TestBed.inject(Router); - router.navigateByUrl('/throwing').catch(() => null); - advance(fixture); - expect(location.path()).toEqual('/second'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); + router.navigateByUrl('/throwing').catch(() => null); + advance(fixture); + expect(location.path()).toEqual('/second'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2})); - location.back(); - advance(fixture); - expect(location.path()).toEqual('/first'); - expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); - })); + location.back(); + advance(fixture); + expect(location.path()).toEqual('/first'); + expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1})); + })); } }); @@ -522,18 +512,13 @@ function createRoot(router: Router, type: Type): ComponentFixture { } @Component({selector: 'simple-cmp', template: `simple`}) -class SimpleCmp { -} +class SimpleCmp {} -@NgModule( - {imports: [RouterModule.forChild([{path: '', component: SimpleCmp}])]}, - ) -class ModuleWithSimpleCmpAsRoute { -} +@NgModule({imports: [RouterModule.forChild([{path: '', component: SimpleCmp}])]}) +class ModuleWithSimpleCmpAsRoute {} @Component({selector: 'root-cmp', template: ``}) -class RootCmp { -} +class RootCmp {} @Component({selector: 'throwing-cmp', template: ''}) class ThrowingCmp { @@ -542,23 +527,15 @@ class ThrowingCmp { } } - - function advance(fixture: ComponentFixture, millis?: number): void { tick(millis); fixture.detectChanges(); } @NgModule({ - imports: [ - RouterOutlet, - CommonModule, - ], - providers: [ - provideLocationMocks(), - ], + imports: [RouterOutlet, CommonModule], + providers: [provideLocationMocks()], exports: [SimpleCmp, RootCmp, ThrowingCmp], - declarations: [SimpleCmp, RootCmp, ThrowingCmp] + declarations: [SimpleCmp, RootCmp, ThrowingCmp], }) -class TestModule { -} +class TestModule {} diff --git a/packages/router/test/config.spec.ts b/packages/router/test/config.spec.ts index 2c74cd0f439..14249046e61 100644 --- a/packages/router/test/config.spec.ts +++ b/packages/router/test/config.spec.ts @@ -13,9 +13,12 @@ import {validateConfig} from '../src/utils/config'; describe('config', () => { describe('validateConfig', () => { it('should not throw when no errors', () => { - expect( - () => validateConfig([{path: 'a', redirectTo: 'b'}, {path: 'b', component: ComponentA}])) - .not.toThrow(); + expect(() => + validateConfig([ + {path: 'a', redirectTo: 'b'}, + {path: 'b', component: ComponentA}, + ]), + ).not.toThrow(); }); it('should not throw when a matcher is provided', () => { @@ -24,20 +27,22 @@ describe('config', () => { it('should throw for undefined route', () => { expect(() => { - validateConfig( - [{path: 'a', component: ComponentA}, , {path: 'b', component: ComponentB}] as Routes); + validateConfig([ + {path: 'a', component: ComponentA}, + , + {path: 'b', component: ComponentB}, + ] as Routes); }).toThrowError(/Invalid configuration of route ''/); }); it('should throw for undefined route in children', () => { expect(() => { - validateConfig([{ - path: 'a', - children: [ - {path: 'b', component: ComponentB}, - , - ] - }] as Routes); + validateConfig([ + { + path: 'a', + children: [{path: 'b', component: ComponentB}, ,], + }, + ] as Routes); }).toThrowError(/Invalid configuration of route 'a'/); }); @@ -45,15 +50,19 @@ describe('config', () => { expect(() => { validateConfig([ {path: 'a', component: ComponentA}, - [{path: 'b', component: ComponentB}, {path: 'c', component: ComponentC}] + [ + {path: 'b', component: ComponentB}, + {path: 'c', component: ComponentC}, + ], ] as Routes); }).toThrowError(); }); it('should throw when redirectTo and children are used together', () => { expect(() => { - validateConfig( - [{path: 'a', redirectTo: 'b', children: [{path: 'b', component: ComponentA}]}]); + validateConfig([ + {path: 'a', redirectTo: 'b', children: [{path: 'b', component: ComponentA}]}, + ]); }).toThrowError(); }); @@ -62,11 +71,11 @@ describe('config', () => { }); it('should properly report deeply nested path', () => { - expect( - () => validateConfig([ - {path: 'a', children: [{path: 'b', children: [{path: 'c', children: [{path: 'd'}]}]}]} - ])) - .toThrowError(); + expect(() => + validateConfig([ + {path: 'a', children: [{path: 'b', children: [{path: 'c', children: [{path: 'd'}]}]}]}, + ]), + ).toThrowError(); }); it('should throw when redirectTo and loadChildren are used together', () => { @@ -84,9 +93,11 @@ describe('config', () => { it('should throw when component and redirectTo are used together', () => { expect(() => { validateConfig([{path: 'a', component: ComponentA, redirectTo: 'b'}]); - }) - .toThrowError(new RegExp( - `Invalid configuration of route 'a': redirectTo and component/loadComponent cannot be used together`)); + }).toThrowError( + new RegExp( + `Invalid configuration of route 'a': redirectTo and component/loadComponent cannot be used together`, + ), + ); }); it('should throw when redirectTo and loadComponent are used together', () => { @@ -131,22 +142,22 @@ describe('config', () => { }).toThrowError(); }); - it('should throw when emptyPath is used with redirectTo without explicitly providing matching', - () => { - expect(() => { - validateConfig([{path: '', redirectTo: 'b'}]); - }).toThrowError(/Invalid configuration of route '{path: "", redirectTo: "b"}'/); - }); + it('should throw when emptyPath is used with redirectTo without explicitly providing matching', () => { + expect(() => { + validateConfig([{path: '', redirectTo: 'b'}]); + }).toThrowError(/Invalid configuration of route '{path: "", redirectTo: "b"}'/); + }); it('should throw when path/outlet combination is invalid', () => { expect(() => { validateConfig([{path: 'a', outlet: 'aux'}]); - }) - .toThrowError( - /Invalid configuration of route 'a': a componentless route without children or loadChildren cannot have a named outlet set/); + }).toThrowError( + /Invalid configuration of route 'a': a componentless route without children or loadChildren cannot have a named outlet set/, + ); expect(() => validateConfig([{path: 'a', outlet: '', children: []}])).not.toThrow(); - expect(() => validateConfig([{path: 'a', outlet: PRIMARY_OUTLET, children: []}])) - .not.toThrow(); + expect(() => + validateConfig([{path: 'a', outlet: PRIMARY_OUTLET, children: []}]), + ).not.toThrow(); }); it('should not throw when path/outlet combination is valid', () => { diff --git a/packages/router/test/create_router_state.spec.ts b/packages/router/test/create_router_state.spec.ts index ed5e8f73706..52793422e42 100644 --- a/packages/router/test/create_router_state.spec.ts +++ b/packages/router/test/create_router_state.spec.ts @@ -15,7 +15,13 @@ import {Routes} from '../src/models'; import {recognize} from '../src/recognize'; import {DefaultRouteReuseStrategy} from '../src/route_reuse_strategy'; import {RouterConfigLoader} from '../src/router_config_loader'; -import {ActivatedRoute, advanceActivatedRoute, createEmptyState, RouterState, RouterStateSnapshot} from '../src/router_state'; +import { + ActivatedRoute, + advanceActivatedRoute, + createEmptyState, + RouterState, + RouterStateSnapshot, +} from '../src/router_state'; import {PRIMARY_OUTLET} from '../src/shared'; import {DefaultUrlSerializer, UrlTree} from '../src/url_tree'; import {TreeNode} from '../src/utils/tree'; @@ -30,15 +36,17 @@ describe('create router state', async () => { it('should create new state', async () => { const state = createRouterState( - reuseStrategy, - await createState( - [ - {path: 'a', component: ComponentA}, - {path: 'b', component: ComponentB, outlet: 'left'}, - {path: 'c', component: ComponentC, outlet: 'right'} - ], - 'a(left:b//right:c)'), - emptyState()); + reuseStrategy, + await createState( + [ + {path: 'a', component: ComponentA}, + {path: 'b', component: ComponentB, outlet: 'left'}, + {path: 'c', component: ComponentC, outlet: 'right'}, + ], + 'a(left:b//right:c)', + ), + emptyState(), + ); checkActivatedRoute(state.root, RootComponent); @@ -50,15 +58,22 @@ describe('create router state', async () => { it('should reuse existing nodes when it can', async () => { const config = [ - {path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'}, - {path: 'c', component: ComponentC, outlet: 'left'} + {path: 'a', component: ComponentA}, + {path: 'b', component: ComponentB, outlet: 'left'}, + {path: 'c', component: ComponentC, outlet: 'left'}, ]; - const prevState = - createRouterState(reuseStrategy, await createState(config, 'a(left:b)'), emptyState()); + const prevState = createRouterState( + reuseStrategy, + await createState(config, 'a(left:b)'), + emptyState(), + ); advanceState(prevState); - const state = - createRouterState(reuseStrategy, await createState(config, 'a(left:c)'), prevState); + const state = createRouterState( + reuseStrategy, + await createState(config, 'a(left:c)'), + prevState, + ); expect(prevState.root).toBe(state.root); const prevC = (prevState as any).children(prevState.root); @@ -70,18 +85,27 @@ describe('create router state', async () => { }); it('should handle componentless routes', async () => { - const config = [{ - path: 'a/:id', - children: - [{path: 'b', component: ComponentA}, {path: 'c', component: ComponentB, outlet: 'right'}] - }]; - + const config = [ + { + path: 'a/:id', + children: [ + {path: 'b', component: ComponentA}, + {path: 'c', component: ComponentB, outlet: 'right'}, + ], + }, + ]; const prevState = createRouterState( - reuseStrategy, await createState(config, 'a/1;p=11/(b//right:c)'), emptyState()); + reuseStrategy, + await createState(config, 'a/1;p=11/(b//right:c)'), + emptyState(), + ); advanceState(prevState); const state = createRouterState( - reuseStrategy, await createState(config, 'a/2;p=22/(b//right:c)'), prevState); + reuseStrategy, + await createState(config, 'a/2;p=22/(b//right:c)'), + prevState, + ); expect(prevState.root).toBe(state.root); const prevP = (prevState as any).firstChild(prevState.root)!; @@ -99,13 +123,17 @@ describe('create router state', async () => { it('should not retrieve routes when `shouldAttach` is always false', async () => { const config: Routes = [ - {path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'}, - {path: 'c', component: ComponentC, outlet: 'left'} + {path: 'a', component: ComponentA}, + {path: 'b', component: ComponentB, outlet: 'left'}, + {path: 'c', component: ComponentC, outlet: 'left'}, ]; spyOn(reuseStrategy, 'retrieve'); - const prevState = - createRouterState(reuseStrategy, await createState(config, 'a(left:b)'), emptyState()); + const prevState = createRouterState( + reuseStrategy, + await createState(config, 'a(left:b)'), + emptyState(), + ); advanceState(prevState); createRouterState(reuseStrategy, await createState(config, 'a(left:c)'), prevState); expect(reuseStrategy.retrieve).not.toHaveBeenCalled(); @@ -114,11 +142,14 @@ describe('create router state', async () => { it('should consistently represent future and current state', async () => { const config: Routes = [ {path: '', pathMatch: 'full', component: ComponentA}, - {path: 'product/:id', component: ComponentB} + {path: 'product/:id', component: ComponentB}, ]; spyOn(reuseStrategy, 'shouldReuseRoute').and.callThrough(); - const previousState = - createRouterState(reuseStrategy, await createState(config, ''), emptyState()); + const previousState = createRouterState( + reuseStrategy, + await createState(config, ''), + emptyState(), + ); advanceState(previousState); (reuseStrategy.shouldReuseRoute as jasmine.Spy).calls.reset(); @@ -151,14 +182,22 @@ function advanceNode(node: TreeNode): void { async function createState(config: Routes, url: string): Promise { return recognize( - TestBed.inject(EnvironmentInjector), TestBed.inject(RouterConfigLoader), RootComponent, - config, tree(url), new DefaultUrlSerializer()) - .pipe(map(result => result.state)) - .toPromise() as Promise; + TestBed.inject(EnvironmentInjector), + TestBed.inject(RouterConfigLoader), + RootComponent, + config, + tree(url), + new DefaultUrlSerializer(), + ) + .pipe(map((result) => result.state)) + .toPromise() as Promise; } function checkActivatedRoute( - actual: ActivatedRoute, cmp: Function, outlet: string = PRIMARY_OUTLET): void { + actual: ActivatedRoute, + cmp: Function, + outlet: string = PRIMARY_OUTLET, +): void { if (actual === null) { expect(actual).toBeDefined(); } else { diff --git a/packages/router/test/create_url_tree.spec.ts b/packages/router/test/create_url_tree.spec.ts index e0a4b7c902c..61afd5a29a2 100644 --- a/packages/router/test/create_url_tree.spec.ts +++ b/packages/router/test/create_url_tree.spec.ts @@ -29,7 +29,7 @@ describe('createUrlTree', async () => { children: [ {path: 'child', component: class {}}, {path: '**', outlet: 'secondary', component: class {}}, - ] + ], }, { path: 'a', @@ -37,7 +37,7 @@ describe('createUrlTree', async () => { {path: '**', component: class {}}, {path: '**', outlet: 'right', component: class {}}, {path: '**', outlet: 'left', component: class {}}, - ] + ], }, {path: '**', component: class {}}, {path: '**', outlet: 'right', component: class {}}, @@ -89,9 +89,9 @@ describe('createUrlTree', async () => { it('should error when navigating to the root segment with params', async () => { const p = serializer.parse('/'); - await expectAsync(createRoot(p, [ - '/', {p: 11} - ])).toBeRejectedWithError(/Root segment cannot have matrix parameters/); + await expectAsync(createRoot(p, ['/', {p: 11}])).toBeRejectedWithError( + /Root segment cannot have matrix parameters/, + ); }); it('should support nested segments', async () => { @@ -155,14 +155,13 @@ describe('createUrlTree', async () => { expect(serializer.serialize(t)).toEqual('/parent/(child//secondary:popup)'); }); - it('should support updating two outlets at the same time relative to non-root segment', - async () => { - await router.navigateByUrl('/parent/child'); - const t = create( - router.routerState.root.children[0], - [{outlets: {primary: 'child', secondary: 'popup'}}]); - expect(serializer.serialize(t)).toEqual('/parent/(child//secondary:popup)'); - }); + it('should support updating two outlets at the same time relative to non-root segment', async () => { + await router.navigateByUrl('/parent/child'); + const t = create(router.routerState.root.children[0], [ + {outlets: {primary: 'child', secondary: 'popup'}}, + ]); + expect(serializer.serialize(t)).toEqual('/parent/(child//secondary:popup)'); + }); it('should support adding multiple outlets with prefix', async () => { const p = serializer.parse(''); @@ -205,63 +204,68 @@ describe('createUrlTree', async () => { expect(serializer.serialize(t)).toEqual('/parent/child(rootSecondary:rootPopup)'); }); - it('works with named children of empty path primary, relative to non-empty parent', - async () => { - router.resetConfig([{ - path: 'case', - component: class {}, - children: [ - { - path: '', - component: class {}, - children: [ - {path: 'foo', outlet: 'foo', children: []}, - ], - }, - ] - }]); - await router.navigateByUrl('/case'); - expect(router.url).toEqual('/case'); - expect(router - .createUrlTree( - [{outlets: {'foo': ['foo']}}], - // relative to the 'case' route - {relativeTo: router.routerState.root.firstChild}) - .toString()) - .toEqual('/case/(foo:foo)'); - }); + it('works with named children of empty path primary, relative to non-empty parent', async () => { + router.resetConfig([ + { + path: 'case', + component: class {}, + children: [ + { + path: '', + component: class {}, + children: [{path: 'foo', outlet: 'foo', children: []}], + }, + ], + }, + ]); + await router.navigateByUrl('/case'); + expect(router.url).toEqual('/case'); + expect( + router + .createUrlTree( + [{outlets: {'foo': ['foo']}}], + // relative to the 'case' route + {relativeTo: router.routerState.root.firstChild}, + ) + .toString(), + ).toEqual('/case/(foo:foo)'); + }); it('can change both primary and named outlets under an empty path', async () => { - router.resetConfig([{ - path: 'foo', - children: [ - { - path: '', - component: class {}, - children: [ - {path: 'bar', component: class {}}, - {path: 'baz', component: class {}, outlet: 'other'}, - ], - }, - ] - }]); + router.resetConfig([ + { + path: 'foo', + children: [ + { + path: '', + component: class {}, + children: [ + {path: 'bar', component: class {}}, + {path: 'baz', component: class {}, outlet: 'other'}, + ], + }, + ], + }, + ]); await router.navigateByUrl('/foo/(bar//other:baz)'); expect(router.url).toEqual('/foo/(bar//other:baz)'); - expect(router - .createUrlTree( - [ - { - outlets: { - other: null, - primary: ['bar'], - }, - }, - ], - // relative to the root '' route - {relativeTo: router.routerState.root.firstChild}) - .toString()) - .toEqual('/foo/bar'); + expect( + router + .createUrlTree( + [ + { + outlets: { + other: null, + primary: ['bar'], + }, + }, + ], + // relative to the root '' route + {relativeTo: router.routerState.root.firstChild}, + ) + .toString(), + ).toEqual('/foo/bar'); }); describe('absolute navigations', () => { @@ -269,15 +273,14 @@ describe('createUrlTree', async () => { router.resetConfig([ { path: '', - children: [ - {path: '**', outlet: 'left', component: class {}}, - ], + children: [{path: '**', outlet: 'left', component: class {}}], }, ]); await router.navigateByUrl('(left:search)'); expect(router.url).toEqual('/(left:search)'); - expect(router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString()) - .toEqual('/(left:projects/123)'); + expect( + router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString(), + ).toEqual('/(left:projects/123)'); }); it('empty path parent and sibling with a path', async () => { router.resetConfig([ @@ -291,22 +294,27 @@ describe('createUrlTree', async () => { ]); await router.navigateByUrl('/x(left:search)'); expect(router.url).toEqual('/x(left:search)'); - expect(router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString()) - .toEqual('/x(left:projects/123)'); - expect(router - .createUrlTree([ - '/', { - outlets: { - 'primary': [{ - outlets: { - 'left': ['projects', '123'], - } - }] - } - } - ]) - .toString()) - .toEqual('/x(left:projects/123)'); + expect( + router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString(), + ).toEqual('/x(left:projects/123)'); + expect( + router + .createUrlTree([ + '/', + { + outlets: { + 'primary': [ + { + outlets: { + 'left': ['projects', '123'], + }, + }, + ], + }, + }, + ]) + .toString(), + ).toEqual('/x(left:projects/123)'); }); it('empty path parent and sibling', async () => { @@ -322,49 +330,52 @@ describe('createUrlTree', async () => { ]); await router.navigateByUrl('/(left:search//right:define)'); expect(router.url).toEqual('/(left:search//right:define)'); - expect(router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString()) - .toEqual('/(left:projects/123//right:define)'); + expect( + router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString(), + ).toEqual('/(left:projects/123//right:define)'); }); it('two pathless parents', async () => { router.resetConfig([ { path: '', - children: [{ - path: '', - children: [ - {path: '**', outlet: 'left', component: class {}}, - ] - }], + children: [ + { + path: '', + children: [{path: '**', outlet: 'left', component: class {}}], + }, + ], }, ]); await router.navigateByUrl('(left:search)'); expect(router.url).toEqual('/(left:search)'); - expect(router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString()) - .toEqual('/(left:projects/123)'); + expect( + router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString(), + ).toEqual('/(left:projects/123)'); }); it('maintains structure when primary outlet is not pathless', async () => { router.resetConfig([ { path: 'a', - children: [ - {path: '**', outlet: 'left', component: class {}}, - ], + children: [{path: '**', outlet: 'left', component: class {}}], }, {path: '**', outlet: 'left', component: class {}}, ]); await router.navigateByUrl('/a/(left:search)'); expect(router.url).toEqual('/a/(left:search)'); - expect(router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString()) - .toEqual('/a/(left:search)(left:projects/123)'); + expect( + router.createUrlTree(['/', {outlets: {'left': ['projects', '123']}}]).toString(), + ).toEqual('/a/(left:search)(left:projects/123)'); }); }); }); it('can navigate to nested route where commands is string', async () => { const p = serializer.parse('/'); - const t = await createRoot( - p, ['/', {outlets: {primary: ['child', {outlets: {primary: 'nested-primary'}}]}}]); + const t = await createRoot(p, [ + '/', + {outlets: {primary: ['child', {outlets: {primary: 'nested-primary'}}]}}, + ]); expect(serializer.serialize(t)).toEqual('/child/nested-primary'); }); @@ -522,8 +533,9 @@ describe('createUrlTree', async () => { it('should support updating secondary segments', async () => { await router.navigateByUrl('/a/b'); - const t = - create(router.routerState.root.children[0].children[0], [{outlets: {right: ['c']}}]); + const t = create(router.routerState.root.children[0].children[0], [ + {outlets: {right: ['c']}}, + ]); expect(serializer.serialize(t)).toEqual('/a/b/(right:c)'); }); }); @@ -548,7 +560,13 @@ describe('createUrlTree', async () => { it('should support pathless child of pathless root', async () => { router.resetConfig([ - {path: '', children: [{path: '', component: class {}}, {path: 'lazy', component: class {}}]} + { + path: '', + children: [ + {path: '', component: class {}}, + {path: 'lazy', component: class {}}, + ], + }, ]); await router.navigateByUrl('/'); const t = create(router.routerState.root.children[0].children[0], ['lazy']); @@ -557,117 +575,131 @@ describe('createUrlTree', async () => { }); async function createRoot( - tree: UrlTree, commands: any[], queryParams?: Params, fragment?: string): Promise { + tree: UrlTree, + commands: any[], + queryParams?: Params, + fragment?: string, +): Promise { const router = TestBed.inject(Router); await router.navigateByUrl(tree); - return router.createUrlTree( - commands, {relativeTo: router.routerState.root, queryParams, fragment}); + return router.createUrlTree(commands, { + relativeTo: router.routerState.root, + queryParams, + fragment, + }); } function create( - relativeTo: ActivatedRoute, commands: any[], queryParams?: Params, fragment?: string) { + relativeTo: ActivatedRoute, + commands: any[], + queryParams?: Params, + fragment?: string, +) { return TestBed.inject(Router).createUrlTree(commands, {relativeTo, queryParams, fragment}); } describe('createUrlTreeFromSnapshot', async () => { it('can create a UrlTree relative to empty path named parent', fakeAsync(() => { - @Component({ - template: ``, - standalone: true, - imports: [RouterModule], - }) - class MainPageComponent { - constructor(private route: ActivatedRoute, private router: Router) {} + @Component({ + template: ``, + standalone: true, + imports: [RouterModule], + }) + class MainPageComponent { + constructor( + private route: ActivatedRoute, + private router: Router, + ) {} - navigate() { - this.router.navigateByUrl( - createUrlTreeFromSnapshot(this.route.snapshot, ['innerRoute'], null, null)); - } - } + navigate() { + this.router.navigateByUrl( + createUrlTreeFromSnapshot(this.route.snapshot, ['innerRoute'], null, null), + ); + } + } - @Component({template: 'child works!'}) - class ChildComponent { - } + @Component({template: 'child works!'}) + class ChildComponent {} - @Component({ - template: '', - standalone: true, - imports: [RouterModule] - }) - class RootCmp { - } + @Component({ + template: '', + standalone: true, + imports: [RouterModule], + }) + class RootCmp {} - const routes: Routes = [{ - path: '', - component: MainPageComponent, - outlet: 'main-page', - children: [{path: 'innerRoute', component: ChildComponent}] - }]; + const routes: Routes = [ + { + path: '', + component: MainPageComponent, + outlet: 'main-page', + children: [{path: 'innerRoute', component: ChildComponent}], + }, + ]; - TestBed.configureTestingModule({imports: [RouterModule.forRoot(routes)]}); - const router = TestBed.inject(Router); - const fixture = TestBed.createComponent(RootCmp); + TestBed.configureTestingModule({imports: [RouterModule.forRoot(routes)]}); + const router = TestBed.inject(Router); + const fixture = TestBed.createComponent(RootCmp); - router.initialNavigation(); - advance(fixture); - fixture.debugElement.query(By.directive(MainPageComponent)).componentInstance.navigate(); - advance(fixture); - expect(fixture.nativeElement.innerHTML).toContain('child works!'); - })); + router.initialNavigation(); + advance(fixture); + fixture.debugElement.query(By.directive(MainPageComponent)).componentInstance.navigate(); + advance(fixture); + expect(fixture.nativeElement.innerHTML).toContain('child works!'); + })); it('can navigate to relative to `ActivatedRouteSnapshot` in guard', fakeAsync(() => { - @Injectable({providedIn: 'root'}) - class Guard { - constructor(private readonly router: Router) {} - canActivate(snapshot: ActivatedRouteSnapshot) { - this.router.navigateByUrl( - createUrlTreeFromSnapshot(snapshot, ['../sibling'], null, null)); - } - } + @Injectable({providedIn: 'root'}) + class Guard { + constructor(private readonly router: Router) {} + canActivate(snapshot: ActivatedRouteSnapshot) { + this.router.navigateByUrl(createUrlTreeFromSnapshot(snapshot, ['../sibling'], null, null)); + } + } - @Component({ - template: `main`, - standalone: true, - imports: [RouterModule], - }) - class GuardedComponent { - } + @Component({ + template: `main`, + standalone: true, + imports: [RouterModule], + }) + class GuardedComponent {} - @Component({template: 'sibling', standalone: true}) - class SiblingComponent { - } + @Component({template: 'sibling', standalone: true}) + class SiblingComponent {} - @Component( - {template: '', standalone: true, imports: [RouterModule]}) - class RootCmp { - } + @Component({ + template: '', + standalone: true, + imports: [RouterModule], + }) + class RootCmp {} - const routes: Routes = [ - { - path: 'parent', - component: RootCmp, - children: [ - { - path: 'guarded', - component: GuardedComponent, - canActivate: [Guard], - }, - { - path: 'sibling', - component: SiblingComponent, - } - ], - }, - ]; + const routes: Routes = [ + { + path: 'parent', + component: RootCmp, + children: [ + { + path: 'guarded', + component: GuardedComponent, + canActivate: [Guard], + }, + { + path: 'sibling', + component: SiblingComponent, + }, + ], + }, + ]; - TestBed.configureTestingModule({imports: [RouterModule.forRoot(routes)]}); - const router = TestBed.inject(Router); - const fixture = TestBed.createComponent(RootCmp); + TestBed.configureTestingModule({imports: [RouterModule.forRoot(routes)]}); + const router = TestBed.inject(Router); + const fixture = TestBed.createComponent(RootCmp); - router.navigateByUrl('parent/guarded'); - advance(fixture); - expect(router.url).toEqual('/parent/sibling'); - })); + router.navigateByUrl('parent/guarded'); + advance(fixture); + expect(router.url).toEqual('/parent/sibling'); + })); }); function advance(fixture: ComponentFixture) { diff --git a/packages/router/test/default_export_component.ts b/packages/router/test/default_export_component.ts index 4231dfb4453..673bd6acafa 100644 --- a/packages/router/test/default_export_component.ts +++ b/packages/router/test/default_export_component.ts @@ -13,5 +13,4 @@ import {Component} from '@angular/core'; template: 'default exported', selector: 'test-route', }) -export default class TestRoute { -} +export default class TestRoute {} diff --git a/packages/router/test/default_export_routes.ts b/packages/router/test/default_export_routes.ts index 6dbcb3abd1c..e40931be82f 100644 --- a/packages/router/test/default_export_routes.ts +++ b/packages/router/test/default_export_routes.ts @@ -14,10 +14,6 @@ import {Routes} from '@angular/router'; template: 'default exported', selector: 'test-route', }) -export class TestRoute { -} +export class TestRoute {} - -export default [ - {path: '', pathMatch: 'full', component: TestRoute}, -] as Routes; +export default [{path: '', pathMatch: 'full', component: TestRoute}] as Routes; diff --git a/packages/router/test/directives/router_outlet.spec.ts b/packages/router/test/directives/router_outlet.spec.ts index 97b78f98c73..4a9c7de8a35 100644 --- a/packages/router/test/directives/router_outlet.spec.ts +++ b/packages/router/test/directives/router_outlet.spec.ts @@ -9,185 +9,190 @@ import {CommonModule, NgForOf} from '@angular/common'; import {Component, Input, Type} from '@angular/core'; import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; -import {provideRouter, Router, RouterModule, RouterOutlet, withComponentInputBinding} from '@angular/router/src'; +import { + provideRouter, + Router, + RouterModule, + RouterOutlet, + withComponentInputBinding, +} from '@angular/router/src'; import {RouterTestingHarness} from '@angular/router/testing'; - describe('router outlet name', () => { it('should support name binding', fakeAsync(() => { - @Component({ - standalone: true, - template: '', - imports: [RouterOutlet], - }) - class RootCmp { - name = 'popup'; - } + @Component({ + standalone: true, + template: '', + imports: [RouterOutlet], + }) + class RootCmp { + name = 'popup'; + } - @Component({ - template: 'popup component', - standalone: true, - }) - class PopupCmp { - } + @Component({ + template: 'popup component', + standalone: true, + }) + class PopupCmp {} - TestBed.configureTestingModule( - {imports: [RouterModule.forRoot([{path: '', outlet: 'popup', component: PopupCmp}])]}); - const router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmp); - expect(fixture.nativeElement.innerHTML).toContain('popup component'); - })); + TestBed.configureTestingModule({ + imports: [RouterModule.forRoot([{path: '', outlet: 'popup', component: PopupCmp}])], + }); + const router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmp); + expect(fixture.nativeElement.innerHTML).toContain('popup component'); + })); it('should be able to change the name of the outlet', fakeAsync(() => { - @Component({ - standalone: true, - template: '', - imports: [RouterOutlet], - }) - class RootCmp { - name = ''; - } + @Component({ + standalone: true, + template: '', + imports: [RouterOutlet], + }) + class RootCmp { + name = ''; + } - @Component({ - template: 'hello world', - standalone: true, - }) - class GreetingCmp { - } + @Component({ + template: 'hello world', + standalone: true, + }) + class GreetingCmp {} - @Component({ - template: 'goodbye cruel world', - standalone: true, - }) - class FarewellCmp { - } + @Component({ + template: 'goodbye cruel world', + standalone: true, + }) + class FarewellCmp {} - TestBed.configureTestingModule({ - imports: [RouterModule.forRoot([ - {path: '', outlet: 'greeting', component: GreetingCmp}, - {path: '', outlet: 'farewell', component: FarewellCmp}, - ])] - }); - const router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmp); + TestBed.configureTestingModule({ + imports: [ + RouterModule.forRoot([ + {path: '', outlet: 'greeting', component: GreetingCmp}, + {path: '', outlet: 'farewell', component: FarewellCmp}, + ]), + ], + }); + const router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmp); - expect(fixture.nativeElement.innerHTML).not.toContain('goodbye'); - expect(fixture.nativeElement.innerHTML).not.toContain('hello'); + expect(fixture.nativeElement.innerHTML).not.toContain('goodbye'); + expect(fixture.nativeElement.innerHTML).not.toContain('hello'); - fixture.componentInstance.name = 'greeting'; - advance(fixture); - expect(fixture.nativeElement.innerHTML).toContain('hello'); - expect(fixture.nativeElement.innerHTML).not.toContain('goodbye'); + fixture.componentInstance.name = 'greeting'; + advance(fixture); + expect(fixture.nativeElement.innerHTML).toContain('hello'); + expect(fixture.nativeElement.innerHTML).not.toContain('goodbye'); - fixture.componentInstance.name = 'goodbye'; - advance(fixture); - expect(fixture.nativeElement.innerHTML).toContain('goodbye'); - expect(fixture.nativeElement.innerHTML).not.toContain('hello'); - })); + fixture.componentInstance.name = 'goodbye'; + advance(fixture); + expect(fixture.nativeElement.innerHTML).toContain('goodbye'); + expect(fixture.nativeElement.innerHTML).not.toContain('hello'); + })); it('should support outlets in ngFor', fakeAsync(() => { - @Component({ - standalone: true, - template: ` + @Component({ + standalone: true, + template: `
`, - imports: [RouterOutlet, NgForOf], - }) - class RootCmp { - outlets = ['outlet1', 'outlet2', 'outlet3']; - } + imports: [RouterOutlet, NgForOf], + }) + class RootCmp { + outlets = ['outlet1', 'outlet2', 'outlet3']; + } - @Component({ - template: 'component 1', - standalone: true, - }) - class Cmp1 { - } + @Component({ + template: 'component 1', + standalone: true, + }) + class Cmp1 {} - @Component({ - template: 'component 2', - standalone: true, - }) - class Cmp2 { - } + @Component({ + template: 'component 2', + standalone: true, + }) + class Cmp2 {} - @Component({ - template: 'component 3', - standalone: true, - }) - class Cmp3 { - } + @Component({ + template: 'component 3', + standalone: true, + }) + class Cmp3 {} - TestBed.configureTestingModule({ - imports: [RouterModule.forRoot([ - {path: '1', outlet: 'outlet1', component: Cmp1}, - {path: '2', outlet: 'outlet2', component: Cmp2}, - {path: '3', outlet: 'outlet3', component: Cmp3}, - ])] - }); - const router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmp); + TestBed.configureTestingModule({ + imports: [ + RouterModule.forRoot([ + {path: '1', outlet: 'outlet1', component: Cmp1}, + {path: '2', outlet: 'outlet2', component: Cmp2}, + {path: '3', outlet: 'outlet3', component: Cmp3}, + ]), + ], + }); + const router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmp); - router.navigate([{outlets: {'outlet1': '1'}}]); - advance(fixture); - expect(fixture.nativeElement.innerHTML).toContain('component 1'); - expect(fixture.nativeElement.innerHTML).not.toContain('component 2'); - expect(fixture.nativeElement.innerHTML).not.toContain('component 3'); + router.navigate([{outlets: {'outlet1': '1'}}]); + advance(fixture); + expect(fixture.nativeElement.innerHTML).toContain('component 1'); + expect(fixture.nativeElement.innerHTML).not.toContain('component 2'); + expect(fixture.nativeElement.innerHTML).not.toContain('component 3'); - router.navigate([{outlets: {'outlet1': null, 'outlet2': '2', 'outlet3': '3'}}]); - advance(fixture); - expect(fixture.nativeElement.innerHTML).not.toContain('component 1'); - expect(fixture.nativeElement.innerHTML).toMatch('.*component 2.*component 3'); + router.navigate([{outlets: {'outlet1': null, 'outlet2': '2', 'outlet3': '3'}}]); + advance(fixture); + expect(fixture.nativeElement.innerHTML).not.toContain('component 1'); + expect(fixture.nativeElement.innerHTML).toMatch('.*component 2.*component 3'); - // reverse the outlets - fixture.componentInstance.outlets = ['outlet3', 'outlet2', 'outlet1']; - router.navigate([{outlets: {'outlet1': '1', 'outlet2': '2', 'outlet3': '3'}}]); - advance(fixture); - expect(fixture.nativeElement.innerHTML).toMatch('.*component 3.*component 2.*component 1'); - })); + // reverse the outlets + fixture.componentInstance.outlets = ['outlet3', 'outlet2', 'outlet1']; + router.navigate([{outlets: {'outlet1': '1', 'outlet2': '2', 'outlet3': '3'}}]); + advance(fixture); + expect(fixture.nativeElement.innerHTML).toMatch('.*component 3.*component 2.*component 1'); + })); it('should not activate if route is changed', fakeAsync(() => { - @Component({ - standalone: true, - template: '
', - imports: [RouterOutlet, CommonModule], - }) - class ParentCmp { - initDone = false; - constructor() { - setTimeout(() => this.initDone = true, 1000); - } - } + @Component({ + standalone: true, + template: '
', + imports: [RouterOutlet, CommonModule], + }) + class ParentCmp { + initDone = false; + constructor() { + setTimeout(() => (this.initDone = true), 1000); + } + } - @Component({ - template: 'child component', - standalone: true, - }) - class ChildCmp { - } + @Component({ + template: 'child component', + standalone: true, + }) + class ChildCmp {} - TestBed.configureTestingModule({ - imports: [RouterModule.forRoot([ - {path: 'parent', component: ParentCmp, children: [{path: 'child', component: ChildCmp}]} - ])] - }); - const router = TestBed.inject(Router); - const fixture = createRoot(router, ParentCmp); + TestBed.configureTestingModule({ + imports: [ + RouterModule.forRoot([ + {path: 'parent', component: ParentCmp, children: [{path: 'child', component: ChildCmp}]}, + ]), + ], + }); + const router = TestBed.inject(Router); + const fixture = createRoot(router, ParentCmp); - advance(fixture, 250); - router.navigate(['parent/child']); - advance(fixture, 250); - // Not contain because initDone is still false - expect(fixture.nativeElement.innerHTML).not.toContain('child component'); + advance(fixture, 250); + router.navigate(['parent/child']); + advance(fixture, 250); + // Not contain because initDone is still false + expect(fixture.nativeElement.innerHTML).not.toContain('child component'); - advance(fixture, 1500); - router.navigate(['parent']); - advance(fixture, 1500); - // Not contain because route was changed back to parent - expect(fixture.nativeElement.innerHTML).not.toContain('child component'); - })); + advance(fixture, 1500); + router.navigate(['parent']); + advance(fixture, 1500); + // Not contain because route was changed back to parent + expect(fixture.nativeElement.innerHTML).not.toContain('child component'); + })); }); describe('component input binding', () => { @@ -200,8 +205,9 @@ describe('component input binding', () => { } TestBed.configureTestingModule({ - providers: - [provideRouter([{path: '**', component: MyComponent}], withComponentInputBinding())] + providers: [ + provideRouter([{path: '**', component: MyComponent}], withComponentInputBinding()), + ], }); const harness = await RouterTestingHarness.create(); @@ -228,14 +234,19 @@ describe('component input binding', () => { } TestBed.configureTestingModule({ - providers: [provideRouter( - [{ - path: '**', - component: MyComponent, - data: {'dataA': 'My static data'}, - resolve: {'resolveA': () => 'My resolved data'}, - }], - withComponentInputBinding())] + providers: [ + provideRouter( + [ + { + path: '**', + component: MyComponent, + data: {'dataA': 'My static data'}, + resolve: {'resolveA': () => 'My resolved data'}, + }, + ], + withComponentInputBinding(), + ), + ], }); const harness = await RouterTestingHarness.create(); @@ -253,8 +264,9 @@ describe('component input binding', () => { } TestBed.configureTestingModule({ - providers: - [provideRouter([{path: '**', component: MyComponent}], withComponentInputBinding())] + providers: [ + provideRouter([{path: '**', component: MyComponent}], withComponentInputBinding()), + ], }); const harness = await RouterTestingHarness.create(); @@ -262,59 +274,66 @@ describe('component input binding', () => { expect(instance.language).toEqual('english'); }); - it('when keys conflict, sets inputs based on priority: data > path params > query params', - async () => { - @Component({ - template: '', - }) - class MyComponent { - @Input() result?: string; - } + it('when keys conflict, sets inputs based on priority: data > path params > query params', async () => { + @Component({ + template: '', + }) + class MyComponent { + @Input() result?: string; + } - TestBed.configureTestingModule({ - providers: [provideRouter( - [ - { - path: 'withData', - component: MyComponent, - data: {'result': 'from data'}, - }, - { - path: 'withoutData', - component: MyComponent, - }, - ], - withComponentInputBinding())] - }); - const harness = await RouterTestingHarness.create(); + TestBed.configureTestingModule({ + providers: [ + provideRouter( + [ + { + path: 'withData', + component: MyComponent, + data: {'result': 'from data'}, + }, + { + path: 'withoutData', + component: MyComponent, + }, + ], + withComponentInputBinding(), + ), + ], + }); + const harness = await RouterTestingHarness.create(); - let instance = await harness.navigateByUrl( - '/withData;result=from path param?result=from query params', MyComponent); - expect(instance.result).toEqual('from data'); + let instance = await harness.navigateByUrl( + '/withData;result=from path param?result=from query params', + MyComponent, + ); + expect(instance.result).toEqual('from data'); - // Same component, different instance because it's a different route - instance = await harness.navigateByUrl( - '/withoutData;result=from path param?result=from query params', MyComponent); - expect(instance.result).toEqual('from path param'); - instance = await harness.navigateByUrl('/withoutData?result=from query params', MyComponent); - expect(instance.result).toEqual('from query params'); - }); + // Same component, different instance because it's a different route + instance = await harness.navigateByUrl( + '/withoutData;result=from path param?result=from query params', + MyComponent, + ); + expect(instance.result).toEqual('from path param'); + instance = await harness.navigateByUrl('/withoutData?result=from query params', MyComponent); + expect(instance.result).toEqual('from query params'); + }); it('does not write multiple times if two sources of conflicting keys both update', async () => { - let resultLog: Array = []; + let resultLog: Array = []; @Component({ template: '', }) class MyComponent { @Input() - set result(v: string|undefined) { + set result(v: string | undefined) { resultLog.push(v); } } TestBed.configureTestingModule({ - providers: - [provideRouter([{path: '**', component: MyComponent}], withComponentInputBinding())] + providers: [ + provideRouter([{path: '**', component: MyComponent}], withComponentInputBinding()), + ], }); const harness = await RouterTestingHarness.create(); @@ -339,19 +358,21 @@ describe('component input binding', () => { imports: [RouterOutlet], standalone: true, }) - class OutletWrapper { - } + class OutletWrapper {} TestBed.configureTestingModule({ - providers: [provideRouter( - [{ - path: 'root', - component: OutletWrapper, - children: [ - {path: '**', component: MyComponent}, - ] - }], - withComponentInputBinding())] + providers: [ + provideRouter( + [ + { + path: 'root', + component: OutletWrapper, + children: [{path: '**', component: MyComponent}], + }, + ], + withComponentInputBinding(), + ), + ], }); const harness = await RouterTestingHarness.create('/root/child?myInput=1'); expect(harness.routeNativeElement!.innerText).toBe('1'); diff --git a/packages/router/test/helpers.ts b/packages/router/test/helpers.ts index 1399fdafa91..c2b13be66b6 100644 --- a/packages/router/test/helpers.ts +++ b/packages/router/test/helpers.ts @@ -27,24 +27,32 @@ export function provideTokenLogger(token: string, returnValue = true as boolean return { provide: token, useFactory: (logger: Logger) => () => (logger.add(token), returnValue), - deps: [Logger] + deps: [Logger], }; } export declare type ARSArgs = { - url?: UrlSegment[], - params?: Params, - queryParams?: Params, - fragment?: string, - data?: Data, - outlet?: string, component: Type| string | null, - routeConfig?: Route | null, - resolve?: ResolveData + url?: UrlSegment[]; + params?: Params; + queryParams?: Params; + fragment?: string; + data?: Data; + outlet?: string; + component: Type | string | null; + routeConfig?: Route | null; + resolve?: ResolveData; }; export function createActivatedRouteSnapshot(args: ARSArgs): ActivatedRouteSnapshot { return new (ActivatedRouteSnapshot as any)( - args.url || [], args.params || {}, args.queryParams || null, args.fragment || null, - args.data || null, args.outlet || null, args.component, args.routeConfig || {}, - args.resolve || {}); + args.url || [], + args.params || {}, + args.queryParams || null, + args.fragment || null, + args.data || null, + args.outlet || null, + args.component, + args.routeConfig || {}, + args.resolve || {}, + ); } diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index 8f4f0c92040..8b6d5481551 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -8,11 +8,71 @@ import {CommonModule, HashLocationStrategy, Location, LocationStrategy} from '@angular/common'; import {ɵprovideFakePlatformNavigation} from '@angular/common/testing'; -import {ChangeDetectionStrategy, Component, EnvironmentInjector, inject as coreInject, Inject, Injectable, InjectionToken, NgModule, NgModuleRef, NgZone, OnDestroy, QueryList, Type, ViewChildren, ɵConsole as Console, ɵNoopNgZone as NoopNgZone} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EnvironmentInjector, + inject as coreInject, + Inject, + Injectable, + InjectionToken, + NgModule, + NgModuleRef, + NgZone, + OnDestroy, + QueryList, + Type, + ViewChildren, + ɵConsole as Console, + ɵNoopNgZone as NoopNgZone, +} from '@angular/core'; import {ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing'; import {By} from '@angular/platform-browser/src/dom/debug/by'; import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {ActivatedRoute, ActivatedRouteSnapshot, ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, DefaultUrlSerializer, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, Navigation, NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationStart, ParamMap, Params, PreloadAllModules, PreloadingStrategy, PRIMARY_OUTLET, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouteReuseStrategy, RouterEvent, RouterLink, RouterLinkActive, RouterModule, RouterOutlet, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegment, UrlSegmentGroup, UrlTree} from '@angular/router'; +import { + ActivatedRoute, + ActivatedRouteSnapshot, + ActivationEnd, + ActivationStart, + ChildActivationEnd, + ChildActivationStart, + DefaultUrlSerializer, + DetachedRouteHandle, + Event, + GuardsCheckEnd, + GuardsCheckStart, + Navigation, + NavigationCancel, + NavigationCancellationCode, + NavigationEnd, + NavigationError, + NavigationSkipped, + NavigationStart, + ParamMap, + Params, + PreloadAllModules, + PreloadingStrategy, + PRIMARY_OUTLET, + ResolveEnd, + ResolveStart, + RouteConfigLoadEnd, + RouteConfigLoadStart, + Router, + RouteReuseStrategy, + RouterEvent, + RouterLink, + RouterLinkActive, + RouterModule, + RouterOutlet, + RouterPreloader, + RouterStateSnapshot, + RoutesRecognized, + RunGuardsAndResolvers, + UrlHandlingStrategy, + UrlSegment, + UrlSegmentGroup, + UrlTree, +} from '@angular/router'; import {RouterTestingHarness} from '@angular/router/testing'; import {concat, EMPTY, firstValueFrom, Observable, Observer, of, Subscription} from 'rxjs'; import {delay, filter, first, last, map, mapTo, takeWhile, tap} from 'rxjs/operators'; @@ -35,65 +95,38 @@ for (const browserAPI of ['navigation', 'history'] as const) { {provide: Console, useValue: noopConsole}, provideRouter([{path: 'simple', component: SimpleCmp}]), browserAPI === 'navigation' ? ɵprovideFakePlatformNavigation() : [], - ] + ], }); }); - it('should navigate with a provided config', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should navigate with a provided config', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.navigateByUrl('/simple'); - advance(fixture); + router.navigateByUrl('/simple'); + advance(fixture); - expect(location.path()).toEqual('/simple'); - }))); + expect(location.path()).toEqual('/simple'); + }), + )); - it('should navigate from ngOnInit hook', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - router.resetConfig([ - {path: '', component: SimpleCmp}, - {path: 'one', component: RouteCmp}, - ]); + it('should navigate from ngOnInit hook', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + router.resetConfig([ + {path: '', component: SimpleCmp}, + {path: 'one', component: RouteCmp}, + ]); - const fixture = createRoot(router, RootCmpWithOnInit); - expect(location.path()).toEqual('/one'); - expect(fixture.nativeElement).toHaveText('route'); - }))); + const fixture = createRoot(router, RootCmpWithOnInit); + expect(location.path()).toEqual('/one'); + expect(fixture.nativeElement).toHaveText('route'); + }), + )); - describe('navigation', function() { + describe('navigation', function () { it('should navigate to the current URL', fakeAsync(() => { - TestBed.configureTestingModule({ - providers: [ - provideRouter([], withRouterConfig({onSameUrlNavigation: 'reload'})), - ] - }); - const router = TestBed.inject(Router); - router.resetConfig([ - {path: '', component: SimpleCmp}, - {path: 'simple', component: SimpleCmp}, - ]); - - const events: (NavigationStart|NavigationEnd)[] = []; - router.events.subscribe(e => onlyNavigationStartAndEnd(e) && events.push(e)); - - router.navigateByUrl('/simple'); - tick(); - - router.navigateByUrl('/simple'); - tick(); - - expectEvents(events, [ - [NavigationStart, '/simple'], [NavigationEnd, '/simple'], [NavigationStart, '/simple'], - [NavigationEnd, '/simple'] - ]); - })); - - it('should override default onSameUrlNavigation with extras', async () => { TestBed.configureTestingModule({ - providers: [ - provideRouter([], withRouterConfig({onSameUrlNavigation: 'ignore'})), - ] + providers: [provideRouter([], withRouterConfig({onSameUrlNavigation: 'reload'}))], }); const router = TestBed.inject(Router); router.resetConfig([ @@ -101,26 +134,56 @@ for (const browserAPI of ['navigation', 'history'] as const) { {path: 'simple', component: SimpleCmp}, ]); - const events: (NavigationStart|NavigationEnd)[] = []; - router.events.subscribe(e => onlyNavigationStartAndEnd(e) && events.push(e)); + const events: (NavigationStart | NavigationEnd)[] = []; + router.events.subscribe((e) => onlyNavigationStartAndEnd(e) && events.push(e)); + + router.navigateByUrl('/simple'); + tick(); + + router.navigateByUrl('/simple'); + tick(); + + expectEvents(events, [ + [NavigationStart, '/simple'], + [NavigationEnd, '/simple'], + [NavigationStart, '/simple'], + [NavigationEnd, '/simple'], + ]); + })); + + it('should override default onSameUrlNavigation with extras', async () => { + TestBed.configureTestingModule({ + providers: [provideRouter([], withRouterConfig({onSameUrlNavigation: 'ignore'}))], + }); + const router = TestBed.inject(Router); + router.resetConfig([ + {path: '', component: SimpleCmp}, + {path: 'simple', component: SimpleCmp}, + ]); + + const events: (NavigationStart | NavigationEnd)[] = []; + router.events.subscribe((e) => onlyNavigationStartAndEnd(e) && events.push(e)); await router.navigateByUrl('/simple'); await router.navigateByUrl('/simple'); // By default, the second navigation is ignored - expectEvents(events, [[NavigationStart, '/simple'], [NavigationEnd, '/simple']]); + expectEvents(events, [ + [NavigationStart, '/simple'], + [NavigationEnd, '/simple'], + ]); await router.navigateByUrl('/simple', {onSameUrlNavigation: 'reload'}); // We overrode the `onSameUrlNavigation` value. This navigation should be processed. expectEvents(events, [ - [NavigationStart, '/simple'], [NavigationEnd, '/simple'], [NavigationStart, '/simple'], - [NavigationEnd, '/simple'] + [NavigationStart, '/simple'], + [NavigationEnd, '/simple'], + [NavigationStart, '/simple'], + [NavigationEnd, '/simple'], ]); }); it('should override default onSameUrlNavigation with extras', async () => { TestBed.configureTestingModule({ - providers: [ - provideRouter([], withRouterConfig({onSameUrlNavigation: 'reload'})), - ] + providers: [provideRouter([], withRouterConfig({onSameUrlNavigation: 'reload'}))], }); const router = TestBed.inject(Router); router.resetConfig([ @@ -128,14 +191,16 @@ for (const browserAPI of ['navigation', 'history'] as const) { {path: 'simple', component: SimpleCmp}, ]); - const events: (NavigationStart|NavigationEnd)[] = []; - router.events.subscribe(e => onlyNavigationStartAndEnd(e) && events.push(e)); + const events: (NavigationStart | NavigationEnd)[] = []; + router.events.subscribe((e) => onlyNavigationStartAndEnd(e) && events.push(e)); await router.navigateByUrl('/simple'); await router.navigateByUrl('/simple'); expectEvents(events, [ - [NavigationStart, '/simple'], [NavigationEnd, '/simple'], [NavigationStart, '/simple'], - [NavigationEnd, '/simple'] + [NavigationStart, '/simple'], + [NavigationEnd, '/simple'], + [NavigationStart, '/simple'], + [NavigationEnd, '/simple'], ]); events.length = 0; @@ -150,10 +215,12 @@ for (const browserAPI of ['navigation', 'history'] as const) { { path: 'simple', component: SimpleCmp, - canActivate: [() => { - observedInfo = coreInject(Router).getCurrentNavigation()?.extras?.info; - return true; - }] + canActivate: [ + () => { + observedInfo = coreInject(Router).getCurrentNavigation()?.extras?.info; + return true; + }, + ], }, ]); @@ -168,19 +235,20 @@ for (const browserAPI of ['navigation', 'history'] as const) { { path: 'simple', component: SimpleCmp, - canActivate: [() => { - observedInfo = coreInject(Router).getCurrentNavigation()?.extras?.info; - return true; - }] + canActivate: [ + () => { + observedInfo = coreInject(Router).getCurrentNavigation()?.extras?.info; + return true; + }, + ], }, ]); @Component({ standalone: true, imports: [RouterLink], - template: `` + template: ``, }) - class App { - } + class App {} const fixture = TestBed.createComponent(App); fixture.autoDetectChanges(); @@ -200,15 +268,17 @@ for (const browserAPI of ['navigation', 'history'] as const) { { path: 'redirect', component: SimpleCmp, - canActivate: [() => coreInject(Router).parseUrl('/simple')] + canActivate: [() => coreInject(Router).parseUrl('/simple')], }, { path: 'simple', component: SimpleCmp, - canActivate: [() => { - observedInfo = coreInject(Router).getCurrentNavigation()?.extras?.info; - return true; - }] + canActivate: [ + () => { + observedInfo = coreInject(Router).getCurrentNavigation()?.extras?.info; + return true; + }, + ], }, ]); @@ -217,232 +287,249 @@ for (const browserAPI of ['navigation', 'history'] as const) { expect(router.url).toEqual('/simple'); }); - it('should ignore empty paths in relative links', - fakeAsync(inject([Router], (router: Router) => { - router.resetConfig([{ - path: 'foo', - children: [{path: 'bar', children: [{path: '', component: RelativeLinkCmp}]}] - }]); + it('should ignore empty paths in relative links', fakeAsync( + inject([Router], (router: Router) => { + router.resetConfig([ + { + path: 'foo', + children: [{path: 'bar', children: [{path: '', component: RelativeLinkCmp}]}], + }, + ]); - const fixture = createRoot(router, RootCmp); + const fixture = createRoot(router, RootCmp); - router.navigateByUrl('/foo/bar'); - advance(fixture); + router.navigateByUrl('/foo/bar'); + advance(fixture); - const link = fixture.nativeElement.querySelector('a'); - expect(link.getAttribute('href')).toEqual('/foo/simple'); - }))); + const link = fixture.nativeElement.querySelector('a'); + expect(link.getAttribute('href')).toEqual('/foo/simple'); + }), + )); - it('should set the restoredState to null when executing imperative navigations', - fakeAsync(inject([Router], (router: Router) => { - router.resetConfig([ - {path: '', component: SimpleCmp}, - {path: 'simple', component: SimpleCmp}, - ]); + it('should set the restoredState to null when executing imperative navigations', fakeAsync( + inject([Router], (router: Router) => { + router.resetConfig([ + {path: '', component: SimpleCmp}, + {path: 'simple', component: SimpleCmp}, + ]); - const fixture = createRoot(router, RootCmp); - let event: NavigationStart; - router.events.subscribe(e => { - if (e instanceof NavigationStart) { - event = e; - } - }); + const fixture = createRoot(router, RootCmp); + let event: NavigationStart; + router.events.subscribe((e) => { + if (e instanceof NavigationStart) { + event = e; + } + }); - router.navigateByUrl('/simple'); - tick(); + router.navigateByUrl('/simple'); + tick(); - expect(event!.navigationTrigger).toEqual('imperative'); - expect(event!.restoredState).toEqual(null); - }))); + expect(event!.navigationTrigger).toEqual('imperative'); + expect(event!.restoredState).toEqual(null); + }), + )); - it('should set history.state if passed using imperative navigation', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - router.resetConfig([ - {path: '', component: SimpleCmp}, - {path: 'simple', component: SimpleCmp}, - ]); + it('should set history.state if passed using imperative navigation', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + router.resetConfig([ + {path: '', component: SimpleCmp}, + {path: 'simple', component: SimpleCmp}, + ]); - const fixture = createRoot(router, RootCmp); - let navigation: Navigation = null!; - router.events.subscribe(e => { - if (e instanceof NavigationStart) { - navigation = router.getCurrentNavigation()!; - } - }); + const fixture = createRoot(router, RootCmp); + let navigation: Navigation = null!; + router.events.subscribe((e) => { + if (e instanceof NavigationStart) { + navigation = router.getCurrentNavigation()!; + } + }); - router.navigateByUrl('/simple', {state: {foo: 'bar'}}); - tick(); + router.navigateByUrl('/simple', {state: {foo: 'bar'}}); + tick(); - const state = location.getState() as any; - expect(state.foo).toBe('bar'); - expect(state).toEqual({foo: 'bar', navigationId: 2}); - expect(navigation.extras.state).toBeDefined(); - expect(navigation.extras.state).toEqual({foo: 'bar'}); - }))); + const state = location.getState() as any; + expect(state.foo).toBe('bar'); + expect(state).toEqual({foo: 'bar', navigationId: 2}); + expect(navigation.extras.state).toBeDefined(); + expect(navigation.extras.state).toEqual({foo: 'bar'}); + }), + )); - it('should set history.state when navigation with browser back and forward', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - router.resetConfig([ - {path: '', component: SimpleCmp}, - {path: 'simple', component: SimpleCmp}, - ]); + it('should set history.state when navigation with browser back and forward', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + router.resetConfig([ + {path: '', component: SimpleCmp}, + {path: 'simple', component: SimpleCmp}, + ]); - const fixture = createRoot(router, RootCmp); - let navigation: Navigation = null!; - router.events.subscribe(e => { - if (e instanceof NavigationStart) { - navigation = router.getCurrentNavigation()!; - } - }); + const fixture = createRoot(router, RootCmp); + let navigation: Navigation = null!; + router.events.subscribe((e) => { + if (e instanceof NavigationStart) { + navigation = router.getCurrentNavigation()!; + } + }); - let state: Record = {foo: 'bar'}; - router.navigateByUrl('/simple', {state}); - tick(); - location.back(); - tick(); - location.forward(); - tick(); + let state: Record = {foo: 'bar'}; + router.navigateByUrl('/simple', {state}); + tick(); + location.back(); + tick(); + location.forward(); + tick(); - expect(navigation.extras.state).toBeDefined(); - expect(navigation.extras.state).toEqual(state); + expect(navigation.extras.state).toBeDefined(); + expect(navigation.extras.state).toEqual(state); - // Manually set state rather than using navigate() - state = {bar: 'foo'}; - location.replaceState(location.path(), '', state); - location.back(); - tick(); - location.forward(); - tick(); + // Manually set state rather than using navigate() + state = {bar: 'foo'}; + location.replaceState(location.path(), '', state); + location.back(); + tick(); + location.forward(); + tick(); - expect(navigation.extras.state).toBeDefined(); - expect(navigation.extras.state).toEqual(state); - }))); + expect(navigation.extras.state).toBeDefined(); + expect(navigation.extras.state).toEqual(state); + }), + )); - it('should navigate correctly when using `Location#historyGo', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - router.resetConfig([ - {path: 'first', component: SimpleCmp}, - {path: 'second', component: SimpleCmp}, + it('should navigate correctly when using `Location#historyGo', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + router.resetConfig([ + {path: 'first', component: SimpleCmp}, + {path: 'second', component: SimpleCmp}, + ]); - ]); + createRoot(router, RootCmp); - createRoot(router, RootCmp); + router.navigateByUrl('/first'); + tick(); + router.navigateByUrl('/second'); + tick(); + expect(router.url).toEqual('/second'); - router.navigateByUrl('/first'); - tick(); - router.navigateByUrl('/second'); - tick(); - expect(router.url).toEqual('/second'); + location.historyGo(-1); + tick(); + expect(router.url).toEqual('/first'); - location.historyGo(-1); - tick(); - expect(router.url).toEqual('/first'); + location.historyGo(1); + tick(); + expect(router.url).toEqual('/second'); - location.historyGo(1); - tick(); - expect(router.url).toEqual('/second'); + location.historyGo(-100); + tick(); + expect(router.url).toEqual('/second'); - location.historyGo(-100); - tick(); - expect(router.url).toEqual('/second'); + location.historyGo(100); + tick(); + expect(router.url).toEqual('/second'); - location.historyGo(100); - tick(); - expect(router.url).toEqual('/second'); + location.historyGo(0); + tick(); + expect(router.url).toEqual('/second'); - location.historyGo(0); - tick(); - expect(router.url).toEqual('/second'); + location.historyGo(); + tick(); + expect(router.url).toEqual('/second'); + }), + )); - location.historyGo(); - tick(); - expect(router.url).toEqual('/second'); - }))); + it('should not error if state is not {[key: string]: any}', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + router.resetConfig([ + {path: '', component: SimpleCmp}, + {path: 'simple', component: SimpleCmp}, + ]); - it('should not error if state is not {[key: string]: any}', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - router.resetConfig([ - {path: '', component: SimpleCmp}, - {path: 'simple', component: SimpleCmp}, - ]); + const fixture = createRoot(router, RootCmp); + let navigation: Navigation = null!; + router.events.subscribe((e) => { + if (e instanceof NavigationStart) { + navigation = router.getCurrentNavigation()!; + } + }); - const fixture = createRoot(router, RootCmp); - let navigation: Navigation = null!; - router.events.subscribe(e => { - if (e instanceof NavigationStart) { - navigation = router.getCurrentNavigation()!; - } - }); + location.replaceState('', '', 42); + router.navigateByUrl('/simple'); + tick(); + location.back(); + advance(fixture); - location.replaceState('', '', 42); - router.navigateByUrl('/simple'); - tick(); - location.back(); - advance(fixture); + // Angular does not support restoring state to the primitive. + expect(navigation.extras.state).toEqual(undefined); + expect(location.getState()).toEqual({navigationId: 3}); + }), + )); - // Angular does not support restoring state to the primitive. - expect(navigation.extras.state).toEqual(undefined); - expect(location.getState()).toEqual({navigationId: 3}); - }))); + it('should not pollute browser history when replaceUrl is set to true', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + router.resetConfig([ + {path: '', component: SimpleCmp}, + {path: 'a', component: SimpleCmp}, + {path: 'b', component: SimpleCmp}, + ]); - it('should not pollute browser history when replaceUrl is set to true', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - router.resetConfig([ - {path: '', component: SimpleCmp}, {path: 'a', component: SimpleCmp}, - {path: 'b', component: SimpleCmp} - ]); + createRoot(router, RootCmp); - createRoot(router, RootCmp); + const replaceSpy = spyOn(location, 'replaceState'); + router.navigateByUrl('/a', {replaceUrl: true}); + router.navigateByUrl('/b', {replaceUrl: true}); + tick(); - const replaceSpy = spyOn(location, 'replaceState'); - router.navigateByUrl('/a', {replaceUrl: true}); - router.navigateByUrl('/b', {replaceUrl: true}); - tick(); + expect(replaceSpy.calls.count()).toEqual(1); + }), + )); - expect(replaceSpy.calls.count()).toEqual(1); - }))); + it('should skip navigation if another navigation is already scheduled', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + router.resetConfig([ + {path: '', component: SimpleCmp}, + {path: 'a', component: SimpleCmp}, + {path: 'b', component: SimpleCmp}, + ]); - it('should skip navigation if another navigation is already scheduled', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - router.resetConfig([ - {path: '', component: SimpleCmp}, {path: 'a', component: SimpleCmp}, - {path: 'b', component: SimpleCmp} - ]); + const fixture = createRoot(router, RootCmp); - const fixture = createRoot(router, RootCmp); + router.navigate(['/a'], { + queryParams: {a: true}, + queryParamsHandling: 'merge', + replaceUrl: true, + }); + router.navigate(['/b'], { + queryParams: {b: true}, + queryParamsHandling: 'merge', + replaceUrl: true, + }); + tick(); - router.navigate( - ['/a'], {queryParams: {a: true}, queryParamsHandling: 'merge', replaceUrl: true}); - router.navigate( - ['/b'], {queryParams: {b: true}, queryParamsHandling: 'merge', replaceUrl: true}); - tick(); - - /** - * Why do we have '/b?b=true' and not '/b?a=true&b=true'? - * - * This is because the router has the right to stop a navigation mid-flight if another - * navigation has been already scheduled. This is why we can use a top-level guard - * to perform redirects. Calling `navigate` in such a guard will stop the navigation, and - * the components won't be instantiated. - * - * This is a fundamental property of the router: it only cares about its latest state. - * - * This means that components should only map params to something else, not reduce them. - * In other words, the following component is asking for trouble: - * - * ``` - * class MyComponent { - * constructor(a: ActivatedRoute) { - * a.params.scan(...) - * } - * } - * ``` - * - * This also means "queryParamsHandling: 'merge'" should only be used to merge with - * long-living query parameters (e.g., debug). - */ - expect(router.url).toEqual('/b?b=true'); - }))); + /** + * Why do we have '/b?b=true' and not '/b?a=true&b=true'? + * + * This is because the router has the right to stop a navigation mid-flight if another + * navigation has been already scheduled. This is why we can use a top-level guard + * to perform redirects. Calling `navigate` in such a guard will stop the navigation, and + * the components won't be instantiated. + * + * This is a fundamental property of the router: it only cares about its latest state. + * + * This means that components should only map params to something else, not reduce them. + * In other words, the following component is asking for trouble: + * + * ``` + * class MyComponent { + * constructor(a: ActivatedRoute) { + * a.params.scan(...) + * } + * } + * ``` + * + * This also means "queryParamsHandling: 'merge'" should only be used to merge with + * long-living query parameters (e.g., debug). + */ + expect(router.url).toEqual('/b?b=true'); + }), + )); }); describe('navigation warning', () => { @@ -468,23 +555,25 @@ for (const browserAPI of ['navigation', 'history'] as const) { }); describe('with NgZone enabled', () => { - it('should warn when triggered outside Angular zone', - fakeAsync(inject([Router], (router: Router) => { - isInAngularZone = false; - router.navigateByUrl('/simple'); + it('should warn when triggered outside Angular zone', fakeAsync( + inject([Router], (router: Router) => { + isInAngularZone = false; + router.navigateByUrl('/simple'); - expect(warnings.length).toBe(1); - expect(warnings[0]) - .toBe( - `Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?`); - }))); + expect(warnings.length).toBe(1); + expect(warnings[0]).toBe( + `Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?`, + ); + }), + )); - it('should not warn when triggered inside Angular zone', - fakeAsync(inject([Router], (router: Router) => { - router.navigateByUrl('/simple'); + it('should not warn when triggered inside Angular zone', fakeAsync( + inject([Router], (router: Router) => { + router.navigateByUrl('/simple'); - expect(warnings.length).toBe(0); - }))); + expect(warnings.length).toBe(0); + }), + )); }); describe('with NgZone disabled', () => { @@ -492,18 +581,19 @@ for (const browserAPI of ['navigation', 'history'] as const) { TestBed.overrideProvider(NgZone, {useValue: new NoopNgZone()}); }); - it('should not warn when triggered outside Angular zone', - fakeAsync(inject([Router], (router: Router) => { - isInAngularZone = false; - router.navigateByUrl('/simple'); + it('should not warn when triggered outside Angular zone', fakeAsync( + inject([Router], (router: Router) => { + isInAngularZone = false; + router.navigateByUrl('/simple'); - expect(warnings.length).toBe(0); - }))); + expect(warnings.length).toBe(0); + }), + )); }); }); describe('should execute navigations serially', () => { - let log: Array = []; + let log: Array = []; beforeEach(() => { log = []; @@ -515,22 +605,22 @@ for (const browserAPI of ['navigation', 'history'] as const) { useValue: () => { log.push('trueRightAway'); return true; - } + }, }, { provide: 'trueIn2Seconds', useValue: () => { log.push('trueIn2Seconds-start'); - let res: ((value: boolean) => void); - const p = new Promise(r => res = r); + let res: (value: boolean) => void; + const p = new Promise((r) => (res = r)); setTimeout(() => { log.push('trueIn2Seconds-end'); res(true); }, 2000); return p; - } - } - ] + }, + }, + ], }); }); @@ -549,7 +639,7 @@ for (const browserAPI of ['navigation', 'history'] as const) { - ` + `, }) class NamedOutletHost { logDeactivate(route: string) { @@ -589,385 +679,400 @@ for (const browserAPI of ['navigation', 'history'] as const) { @NgModule({ declarations: [Parent, NamedOutletHost, Child1, Child2, Child3], - imports: [RouterModule.forRoot([])] + imports: [RouterModule.forRoot([])], }) - class TestModule { - } + class TestModule {} it('should advance the parent route after deactivating its children', fakeAsync(() => { - TestBed.configureTestingModule({imports: [TestModule]}); - const router = TestBed.inject(Router); - const location = TestBed.inject(Location); - const fixture = createRoot(router, RootCmp); + TestBed.configureTestingModule({imports: [TestModule]}); + const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'parent/:id', - component: Parent, - children: [ - {path: 'child1', component: Child1}, - {path: 'child2', component: Child2}, - ] - }]); + router.resetConfig([ + { + path: 'parent/:id', + component: Parent, + children: [ + {path: 'child1', component: Child1}, + {path: 'child2', component: Child2}, + ], + }, + ]); - router.navigateByUrl('/parent/1/child1'); - advance(fixture); + router.navigateByUrl('/parent/1/child1'); + advance(fixture); - router.navigateByUrl('/parent/2/child2'); - advance(fixture); + router.navigateByUrl('/parent/2/child2'); + advance(fixture); - expect(location.path()).toEqual('/parent/2/child2'); - expect(log).toEqual([ - {id: '1'}, - 'child1 constructor', - 'child1 destroy', - {id: '2'}, - 'child2 constructor', - ]); - })); + expect(location.path()).toEqual('/parent/2/child2'); + expect(log).toEqual([ + {id: '1'}, + 'child1 constructor', + 'child1 destroy', + {id: '2'}, + 'child2 constructor', + ]); + })); it('should deactivate outlet children with componentless parent', fakeAsync(() => { - TestBed.configureTestingModule({imports: [TestModule]}); - const router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmp); + TestBed.configureTestingModule({imports: [TestModule]}); + const router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - { - path: 'named-outlets', - component: NamedOutletHost, - children: [ - { - path: 'home', - children: [ - {path: '', component: Child1, outlet: 'first'}, - {path: '', component: Child2, outlet: 'second'}, - {path: 'primary', component: Child3}, - ] - }, - { - path: 'about', - children: [ - {path: '', component: Child1, outlet: 'first'}, - {path: '', component: Child2, outlet: 'second'}, - ] - }, + router.resetConfig([ + { + path: 'named-outlets', + component: NamedOutletHost, + children: [ + { + path: 'home', + children: [ + {path: '', component: Child1, outlet: 'first'}, + {path: '', component: Child2, outlet: 'second'}, + {path: 'primary', component: Child3}, + ], + }, + { + path: 'about', + children: [ + {path: '', component: Child1, outlet: 'first'}, + {path: '', component: Child2, outlet: 'second'}, + ], + }, + ], + }, + { + path: 'other', + component: Parent, + }, + ]); - ] - }, - { - path: 'other', - component: Parent, - }, - ]); + router.navigateByUrl('/named-outlets/home/primary'); + advance(fixture); + expect(log).toEqual([ + 'child3 constructor', // primary outlet always first + 'child1 constructor', + 'child2 constructor', + ]); + log.length = 0; - router.navigateByUrl('/named-outlets/home/primary'); - advance(fixture); - expect(log).toEqual([ - 'child3 constructor', // primary outlet always first - 'child1 constructor', - 'child2 constructor', - ]); - log.length = 0; + router.navigateByUrl('/named-outlets/about'); + advance(fixture); + expect(log).toEqual([ + 'child3 destroy', + 'primary deactivate', + 'child1 destroy', + 'first deactivate', + 'child2 destroy', + 'second deactivate', + 'child1 constructor', + 'child2 constructor', + ]); + log.length = 0; - router.navigateByUrl('/named-outlets/about'); - advance(fixture); - expect(log).toEqual([ - 'child3 destroy', - 'primary deactivate', - 'child1 destroy', - 'first deactivate', - 'child2 destroy', - 'second deactivate', - 'child1 constructor', - 'child2 constructor', - ]); - log.length = 0; + router.navigateByUrl('/other'); + advance(fixture); + expect(log).toEqual([ + 'child1 destroy', + 'first deactivate', + 'child2 destroy', + 'second deactivate', + // route param subscription from 'Parent' component + {}, + ]); + })); - router.navigateByUrl('/other'); - advance(fixture); - expect(log).toEqual([ - 'child1 destroy', - 'first deactivate', - 'child2 destroy', - 'second deactivate', - // route param subscription from 'Parent' component - {}, - ]); - })); + it('should work between aux outlets under two levels of empty path parents', fakeAsync(() => { + TestBed.configureTestingModule({imports: [TestModule]}); + const router = TestBed.inject(Router); + router.resetConfig([ + { + path: '', + children: [ + { + path: '', + component: NamedOutletHost, + children: [ + {path: 'one', component: Child1, outlet: 'first'}, + {path: 'two', component: Child2, outlet: 'first'}, + ], + }, + ], + }, + ]); - it('should work between aux outlets under two levels of empty path parents', - fakeAsync(() => { - TestBed.configureTestingModule({imports: [TestModule]}); - const router = TestBed.inject(Router); - router.resetConfig([{ - path: '', - children: [ - { - path: '', - component: NamedOutletHost, - children: [ - {path: 'one', component: Child1, outlet: 'first'}, - {path: 'two', component: Child2, outlet: 'first'}, - ] - }, - ] - }]); + const fixture = createRoot(router, RootCmp); - const fixture = createRoot(router, RootCmp); + router.navigateByUrl('/(first:one)'); + advance(fixture); + expect(log).toEqual(['child1 constructor']); - router.navigateByUrl('/(first:one)'); - advance(fixture); - expect(log).toEqual(['child1 constructor']); - - log.length = 0; - router.navigateByUrl('/(first:two)'); - advance(fixture); - expect(log).toEqual([ - 'child1 destroy', - 'first deactivate', - 'child2 constructor', - ]); - })); + log.length = 0; + router.navigateByUrl('/(first:two)'); + advance(fixture); + expect(log).toEqual(['child1 destroy', 'first deactivate', 'child2 constructor']); + })); }); - it('should not wait for prior navigations to start a new navigation', - fakeAsync(inject([Router, Location], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should not wait for prior navigations to start a new navigation', fakeAsync( + inject([Router, Location], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'a', component: SimpleCmp, canActivate: ['trueRightAway', 'trueIn2Seconds']}, - {path: 'b', component: SimpleCmp, canActivate: ['trueRightAway', 'trueIn2Seconds']} - ]); + router.resetConfig([ + {path: 'a', component: SimpleCmp, canActivate: ['trueRightAway', 'trueIn2Seconds']}, + {path: 'b', component: SimpleCmp, canActivate: ['trueRightAway', 'trueIn2Seconds']}, + ]); - router.navigateByUrl('/a'); - tick(100); - fixture.detectChanges(); + router.navigateByUrl('/a'); + tick(100); + fixture.detectChanges(); - router.navigateByUrl('/b'); - tick(100); // 200 - fixture.detectChanges(); + router.navigateByUrl('/b'); + tick(100); // 200 + fixture.detectChanges(); - expect(log).toEqual( - ['trueRightAway', 'trueIn2Seconds-start', 'trueRightAway', 'trueIn2Seconds-start']); + expect(log).toEqual([ + 'trueRightAway', + 'trueIn2Seconds-start', + 'trueRightAway', + 'trueIn2Seconds-start', + ]); - tick(2000); // 2200 - fixture.detectChanges(); + tick(2000); // 2200 + fixture.detectChanges(); - expect(log).toEqual([ - 'trueRightAway', 'trueIn2Seconds-start', 'trueRightAway', 'trueIn2Seconds-start', - 'trueIn2Seconds-end', 'trueIn2Seconds-end' - ]); - }))); + expect(log).toEqual([ + 'trueRightAway', + 'trueIn2Seconds-start', + 'trueRightAway', + 'trueIn2Seconds-start', + 'trueIn2Seconds-end', + 'trueIn2Seconds-end', + ]); + }), + )); }); it('Should work inside ChangeDetectionStrategy.OnPush components', fakeAsync(() => { - @Component({ - selector: 'root-cmp', - template: ``, - changeDetection: ChangeDetectionStrategy.OnPush, - }) - class OnPushOutlet { - } + @Component({ + selector: 'root-cmp', + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class OnPushOutlet {} - @Component({selector: 'need-cd', template: `{{'it works!'}}`}) - class NeedCdCmp { - } + @Component({selector: 'need-cd', template: `{{'it works!'}}`}) + class NeedCdCmp {} - @NgModule({ - declarations: [OnPushOutlet, NeedCdCmp], - imports: [RouterModule.forRoot([])], - }) - class TestModule { - } + @NgModule({ + declarations: [OnPushOutlet, NeedCdCmp], + imports: [RouterModule.forRoot([])], + }) + class TestModule {} - TestBed.configureTestingModule({imports: [TestModule]}); + TestBed.configureTestingModule({imports: [TestModule]}); - const router: Router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmp); + const router: Router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'on', - component: OnPushOutlet, - children: [{ - path: 'push', - component: NeedCdCmp, - }], - }]); + router.resetConfig([ + { + path: 'on', + component: OnPushOutlet, + children: [ + { + path: 'push', + component: NeedCdCmp, + }, + ], + }, + ]); - advance(fixture); - router.navigateByUrl('on'); - advance(fixture); - router.navigateByUrl('on/push'); - advance(fixture); + advance(fixture); + router.navigateByUrl('on'); + advance(fixture); + router.navigateByUrl('on/push'); + advance(fixture); - expect(fixture.nativeElement).toHaveText('it works!'); - })); + expect(fixture.nativeElement).toHaveText('it works!'); + })); - it('should not error when no url left and no children are matching', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should not error when no url left and no children are matching', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [{path: 'simple', component: SimpleCmp}] - }]); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [{path: 'simple', component: SimpleCmp}], + }, + ]); - router.navigateByUrl('/team/33/simple'); - advance(fixture); + router.navigateByUrl('/team/33/simple'); + advance(fixture); - expect(location.path()).toEqual('/team/33/simple'); - expect(fixture.nativeElement).toHaveText('team 33 [ simple, right: ]'); + expect(location.path()).toEqual('/team/33/simple'); + expect(fixture.nativeElement).toHaveText('team 33 [ simple, right: ]'); - router.navigateByUrl('/team/33'); - advance(fixture); + router.navigateByUrl('/team/33'); + advance(fixture); - expect(location.path()).toEqual('/team/33'); - expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]'); - }))); + expect(location.path()).toEqual('/team/33'); + expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]'); + }), + )); - it('should work when an outlet is in an ngIf', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should work when an outlet is in an ngIf', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'child', - component: OutletInNgIf, - children: [{path: 'simple', component: SimpleCmp}] - }]); + router.resetConfig([ + { + path: 'child', + component: OutletInNgIf, + children: [{path: 'simple', component: SimpleCmp}], + }, + ]); - router.navigateByUrl('/child/simple'); - advance(fixture); + router.navigateByUrl('/child/simple'); + advance(fixture); - expect(location.path()).toEqual('/child/simple'); - }))); + expect(location.path()).toEqual('/child/simple'); + }), + )); it('should work when an outlet is added/removed', fakeAsync(() => { - @Component({ - selector: 'someRoot', - template: `[
]` - }) - class RootCmpWithLink { - cond: boolean = true; - } - TestBed.configureTestingModule({declarations: [RootCmpWithLink]}); + @Component({ + selector: 'someRoot', + template: `[
]`, + }) + class RootCmpWithLink { + cond: boolean = true; + } + TestBed.configureTestingModule({declarations: [RootCmpWithLink]}); - const router: Router = TestBed.inject(Router); + const router: Router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmpWithLink); + const fixture = createRoot(router, RootCmpWithLink); - router.resetConfig([ - {path: 'simple', component: SimpleCmp}, - {path: 'blank', component: BlankCmp}, - ]); + router.resetConfig([ + {path: 'simple', component: SimpleCmp}, + {path: 'blank', component: BlankCmp}, + ]); - router.navigateByUrl('/simple'); - advance(fixture); - expect(fixture.nativeElement).toHaveText('[simple]'); + router.navigateByUrl('/simple'); + advance(fixture); + expect(fixture.nativeElement).toHaveText('[simple]'); - fixture.componentInstance.cond = false; - advance(fixture); - expect(fixture.nativeElement).toHaveText('[]'); + fixture.componentInstance.cond = false; + advance(fixture); + expect(fixture.nativeElement).toHaveText('[]'); - fixture.componentInstance.cond = true; - advance(fixture); - expect(fixture.nativeElement).toHaveText('[simple]'); - })); + fixture.componentInstance.cond = true; + advance(fixture); + expect(fixture.nativeElement).toHaveText('[simple]'); + })); it('should update location when navigating', fakeAsync(() => { - @Component({template: `record`}) - class RecordLocationCmp { - private storedPath: string; - constructor(loc: Location) { - this.storedPath = loc.path(); - } - } + @Component({template: `record`}) + class RecordLocationCmp { + private storedPath: string; + constructor(loc: Location) { + this.storedPath = loc.path(); + } + } - @NgModule({declarations: [RecordLocationCmp]}) - class TestModule { - } + @NgModule({declarations: [RecordLocationCmp]}) + class TestModule {} - TestBed.configureTestingModule({imports: [TestModule]}); + TestBed.configureTestingModule({imports: [TestModule]}); - const router = TestBed.inject(Router); - const location = TestBed.inject(Location); - const fixture = createRoot(router, RootCmp); + const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'record/:id', component: RecordLocationCmp}]); + router.resetConfig([{path: 'record/:id', component: RecordLocationCmp}]); - router.navigateByUrl('/record/22'); - advance(fixture); + router.navigateByUrl('/record/22'); + advance(fixture); - const c = fixture.debugElement.children[1].componentInstance; - expect(location.path()).toEqual('/record/22'); - expect(c.storedPath).toEqual('/record/22'); + const c = fixture.debugElement.children[1].componentInstance; + expect(location.path()).toEqual('/record/22'); + expect(c.storedPath).toEqual('/record/22'); - router.navigateByUrl('/record/33'); - advance(fixture); - expect(location.path()).toEqual('/record/33'); - })); + router.navigateByUrl('/record/33'); + advance(fixture); + expect(location.path()).toEqual('/record/33'); + })); - it('should skip location update when using NavigationExtras.skipLocationChange with navigateByUrl', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = TestBed.createComponent(RootCmp); - advance(fixture); + it('should skip location update when using NavigationExtras.skipLocationChange with navigateByUrl', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = TestBed.createComponent(RootCmp); + advance(fixture); - router.resetConfig([{path: 'team/:id', component: TeamCmp}]); + router.resetConfig([{path: 'team/:id', component: TeamCmp}]); - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); - expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]'); + expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]'); - router.navigateByUrl('/team/33', {skipLocationChange: true}); - advance(fixture); + router.navigateByUrl('/team/33', {skipLocationChange: true}); + advance(fixture); - expect(location.path()).toEqual('/team/22'); + expect(location.path()).toEqual('/team/22'); - expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]'); - }))); + expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]'); + }), + )); - it('should skip location update when using NavigationExtras.skipLocationChange with navigate', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = TestBed.createComponent(RootCmp); - advance(fixture); + it('should skip location update when using NavigationExtras.skipLocationChange with navigate', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = TestBed.createComponent(RootCmp); + advance(fixture); - router.resetConfig([{path: 'team/:id', component: TeamCmp}]); + router.resetConfig([{path: 'team/:id', component: TeamCmp}]); - router.navigate(['/team/22']); - advance(fixture); - expect(location.path()).toEqual('/team/22'); + router.navigate(['/team/22']); + advance(fixture); + expect(location.path()).toEqual('/team/22'); - expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]'); + expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]'); - router.navigate(['/team/33'], {skipLocationChange: true}); - advance(fixture); + router.navigate(['/team/33'], {skipLocationChange: true}); + advance(fixture); - expect(location.path()).toEqual('/team/22'); + expect(location.path()).toEqual('/team/22'); - expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]'); - }))); + expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]'); + }), + )); - it('should navigate after navigation with skipLocationChange', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = TestBed.createComponent(RootCmpWithNamedOutlet); - advance(fixture); + it('should navigate after navigation with skipLocationChange', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = TestBed.createComponent(RootCmpWithNamedOutlet); + advance(fixture); - router.resetConfig([{path: 'show', outlet: 'main', component: SimpleCmp}]); + router.resetConfig([{path: 'show', outlet: 'main', component: SimpleCmp}]); - router.navigate([{outlets: {main: 'show'}}], {skipLocationChange: true}); - advance(fixture); - expect(location.path()).toEqual(''); + router.navigate([{outlets: {main: 'show'}}], {skipLocationChange: true}); + advance(fixture); + expect(location.path()).toEqual(''); - expect(fixture.nativeElement).toHaveText('main [simple]'); + expect(fixture.nativeElement).toHaveText('main [simple]'); - router.navigate([{outlets: {main: null}}], {skipLocationChange: true}); - advance(fixture); + router.navigate([{outlets: {main: null}}], {skipLocationChange: true}); + advance(fixture); - expect(location.path()).toEqual(''); + expect(location.path()).toEqual(''); - expect(fixture.nativeElement).toHaveText('main []'); - }))); + expect(fixture.nativeElement).toHaveText('main []'); + }), + )); describe('"eager" urlUpdateStrategy', () => { @Injectable() @@ -992,306 +1097,321 @@ for (const browserAPI of ['navigation', 'history'] as const) { { provide: 'authGuardFail', useValue: (a: any, b: any) => { - return new Promise(res => { + return new Promise((res) => { setTimeout(() => res(serializer.parse('/login')), 1); }); - } + }, }, AuthGuard, DelayedGuard, - ] + ], }); }); - describe('urlUpdateStrategy: eager', () => { beforeEach(() => { - TestBed.configureTestingModule( - {providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))]}); + TestBed.configureTestingModule({ + providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))], + }); }); - it('should eagerly update the URL', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = TestBed.createComponent(RootCmp); - advance(fixture); + it('should eagerly update the URL', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = TestBed.createComponent(RootCmp); + advance(fixture); - router.resetConfig([{path: 'team/:id', component: TeamCmp}]); + router.resetConfig([{path: 'team/:id', component: TeamCmp}]); - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); - expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]'); + expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]'); - router.events.subscribe(e => { - if (!(e instanceof GuardsCheckStart)) { - return; - } - expect(location.path()).toEqual('/team/33'); - expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]'); - return of(null); - }); - router.navigateByUrl('/team/33'); + router.events.subscribe((e) => { + if (!(e instanceof GuardsCheckStart)) { + return; + } + expect(location.path()).toEqual('/team/33'); + expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]'); + return of(null); + }); + router.navigateByUrl('/team/33'); - advance(fixture); - expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]'); - }))); + advance(fixture); + expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]'); + }), + )); - it('should eagerly update the URL', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = TestBed.createComponent(RootCmp); - advance(fixture); + it('should eagerly update the URL', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = TestBed.createComponent(RootCmp); + advance(fixture); - router.resetConfig([ - {path: 'team/:id', component: SimpleCmp, canActivate: ['authGuardFail']}, - {path: 'login', component: AbsoluteSimpleLinkCmp} - ]); + router.resetConfig([ + {path: 'team/:id', component: SimpleCmp, canActivate: ['authGuardFail']}, + {path: 'login', component: AbsoluteSimpleLinkCmp}, + ]); - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); - // Redirects to /login - advance(fixture, 1); - expect(location.path()).toEqual('/login'); + // Redirects to /login + advance(fixture, 1); + expect(location.path()).toEqual('/login'); - // Perform the same logic again, and it should produce the same result - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); + // Perform the same logic again, and it should produce the same result + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); - // Redirects to /login - advance(fixture, 1); - expect(location.path()).toEqual('/login'); - }))); + // Redirects to /login + advance(fixture, 1); + expect(location.path()).toEqual('/login'); + }), + )); - it('should eagerly update URL after redirects are applied', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = TestBed.createComponent(RootCmp); - advance(fixture); + it('should eagerly update URL after redirects are applied', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = TestBed.createComponent(RootCmp); + advance(fixture); - router.resetConfig([{path: 'team/:id', component: TeamCmp}]); + router.resetConfig([{path: 'team/:id', component: TeamCmp}]); - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); - expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]'); + expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]'); - let urlAtNavStart = ''; - let urlAtRoutesRecognized = ''; - router.events.subscribe(e => { - if (e instanceof NavigationStart) { - urlAtNavStart = location.path(); - } - if (e instanceof RoutesRecognized) { - urlAtRoutesRecognized = location.path(); - } - }); + let urlAtNavStart = ''; + let urlAtRoutesRecognized = ''; + router.events.subscribe((e) => { + if (e instanceof NavigationStart) { + urlAtNavStart = location.path(); + } + if (e instanceof RoutesRecognized) { + urlAtRoutesRecognized = location.path(); + } + }); - router.navigateByUrl('/team/33'); + router.navigateByUrl('/team/33'); - advance(fixture); - expect(urlAtNavStart).toBe('/team/22'); - expect(urlAtRoutesRecognized).toBe('/team/33'); - expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]'); - }))); + advance(fixture); + expect(urlAtNavStart).toBe('/team/22'); + expect(urlAtRoutesRecognized).toBe('/team/33'); + expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]'); + }), + )); - it('should set `state`', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - router.resetConfig([ - {path: '', component: SimpleCmp}, - {path: 'simple', component: SimpleCmp}, - ]); + it('should set `state`', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + router.resetConfig([ + {path: '', component: SimpleCmp}, + {path: 'simple', component: SimpleCmp}, + ]); - const fixture = createRoot(router, RootCmp); - let navigation: Navigation = null!; - router.events.subscribe(e => { - if (e instanceof NavigationStart) { - navigation = router.getCurrentNavigation()!; - } - }); + const fixture = createRoot(router, RootCmp); + let navigation: Navigation = null!; + router.events.subscribe((e) => { + if (e instanceof NavigationStart) { + navigation = router.getCurrentNavigation()!; + } + }); - router.navigateByUrl('/simple', {state: {foo: 'bar'}}); - tick(); + router.navigateByUrl('/simple', {state: {foo: 'bar'}}); + tick(); - const state = location.getState() as any; - expect(state).toEqual({foo: 'bar', navigationId: 2}); - expect(navigation.extras.state).toBeDefined(); - expect(navigation.extras.state).toEqual({foo: 'bar'}); - }))); + const state = location.getState() as any; + expect(state).toEqual({foo: 'bar', navigationId: 2}); + expect(navigation.extras.state).toBeDefined(); + expect(navigation.extras.state).toEqual({foo: 'bar'}); + }), + )); it('can renavigate to rejected URL', fakeAsync(() => { - const router = TestBed.inject(Router); - const canActivate = TestBed.inject(AuthGuard); - const location = TestBed.inject(Location); - router.resetConfig([ - {path: '', component: BlankCmp}, - { - path: 'simple', - component: SimpleCmp, - canActivate: [() => coreInject(AuthGuard).canActivate()] - }, - ]); - const fixture = createRoot(router, RootCmp); + const router = TestBed.inject(Router); + const canActivate = TestBed.inject(AuthGuard); + const location = TestBed.inject(Location); + router.resetConfig([ + {path: '', component: BlankCmp}, + { + path: 'simple', + component: SimpleCmp, + canActivate: [() => coreInject(AuthGuard).canActivate()], + }, + ]); + const fixture = createRoot(router, RootCmp); - // Try to navigate to /simple but guard rejects - canActivate.canActivateResult = false; - router.navigateByUrl('/simple'); - advance(fixture); - expect(location.path()).toEqual(''); - expect(fixture.nativeElement.innerHTML).not.toContain('simple'); + // Try to navigate to /simple but guard rejects + canActivate.canActivateResult = false; + router.navigateByUrl('/simple'); + advance(fixture); + expect(location.path()).toEqual(''); + expect(fixture.nativeElement.innerHTML).not.toContain('simple'); - // Renavigate to /simple without guard rejection, should succeed. - canActivate.canActivateResult = true; - router.navigateByUrl('/simple'); - advance(fixture); - expect(location.path()).toEqual('/simple'); - expect(fixture.nativeElement.innerHTML).toContain('simple'); - })); + // Renavigate to /simple without guard rejection, should succeed. + canActivate.canActivateResult = true; + router.navigateByUrl('/simple'); + advance(fixture); + expect(location.path()).toEqual('/simple'); + expect(fixture.nativeElement.innerHTML).toContain('simple'); + })); it('can renavigate to same URL during in-flight navigation', fakeAsync(() => { - const router = TestBed.inject(Router); - const location = TestBed.inject(Location); - router.resetConfig([ - {path: '', component: BlankCmp}, - { - path: 'simple', - component: SimpleCmp, - canActivate: [() => coreInject(DelayedGuard).canActivate()] - }, - ]); - const fixture = createRoot(router, RootCmp); + const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + router.resetConfig([ + {path: '', component: BlankCmp}, + { + path: 'simple', + component: SimpleCmp, + canActivate: [() => coreInject(DelayedGuard).canActivate()], + }, + ]); + const fixture = createRoot(router, RootCmp); - // Start navigating to /simple, but do not flush the guard delay - router.navigateByUrl('/simple'); - tick(); - // eager update strategy so URL is already updated. - expect(location.path()).toEqual('/simple'); - expect(fixture.nativeElement.innerHTML).not.toContain('simple'); + // Start navigating to /simple, but do not flush the guard delay + router.navigateByUrl('/simple'); + tick(); + // eager update strategy so URL is already updated. + expect(location.path()).toEqual('/simple'); + expect(fixture.nativeElement.innerHTML).not.toContain('simple'); - // Start an additional navigation to /simple and ensure at least one of those succeeds. - // It's not super important which one gets processed, but in the past, the router would - // cancel the in-flight one and not process the new one. - router.navigateByUrl('/simple'); - tick(1000); - expect(location.path()).toEqual('/simple'); - expect(fixture.nativeElement.innerHTML).toContain('simple'); - })); + // Start an additional navigation to /simple and ensure at least one of those succeeds. + // It's not super important which one gets processed, but in the past, the router would + // cancel the in-flight one and not process the new one. + router.navigateByUrl('/simple'); + tick(1000); + expect(location.path()).toEqual('/simple'); + expect(fixture.nativeElement.innerHTML).toContain('simple'); + })); }); }); - it('should navigate back and forward', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should navigate back and forward', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: - [{path: 'simple', component: SimpleCmp}, {path: 'user/:name', component: UserCmp}] - }]); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'simple', component: SimpleCmp}, + {path: 'user/:name', component: UserCmp}, + ], + }, + ]); - let event: NavigationStart; - router.events.subscribe(e => { - if (e instanceof NavigationStart) { - event = e; - } - }); + let event: NavigationStart; + router.events.subscribe((e) => { + if (e instanceof NavigationStart) { + event = e; + } + }); - router.navigateByUrl('/team/33/simple'); - advance(fixture); - expect(location.path()).toEqual('/team/33/simple'); - const simpleNavStart = event!; + router.navigateByUrl('/team/33/simple'); + advance(fixture); + expect(location.path()).toEqual('/team/33/simple'); + const simpleNavStart = event!; - router.navigateByUrl('/team/22/user/victor'); - advance(fixture); - const userVictorNavStart = event!; + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); + const userVictorNavStart = event!; + location.back(); + advance(fixture); + expect(location.path()).toEqual('/team/33/simple'); + expect(event!.navigationTrigger).toEqual('popstate'); + expect(event!.restoredState!.navigationId).toEqual(simpleNavStart.id); - location.back(); - advance(fixture); - expect(location.path()).toEqual('/team/33/simple'); - expect(event!.navigationTrigger).toEqual('popstate'); - expect(event!.restoredState!.navigationId).toEqual(simpleNavStart.id); + location.forward(); + advance(fixture); + expect(location.path()).toEqual('/team/22/user/victor'); + expect(event!.navigationTrigger).toEqual('popstate'); + expect(event!.restoredState!.navigationId).toEqual(userVictorNavStart.id); + }), + )); - location.forward(); - advance(fixture); - expect(location.path()).toEqual('/team/22/user/victor'); - expect(event!.navigationTrigger).toEqual('popstate'); - expect(event!.restoredState!.navigationId).toEqual(userVictorNavStart.id); - }))); + it('should navigate to the same url when config changes', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - it('should navigate to the same url when config changes', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + router.resetConfig([{path: 'a', component: SimpleCmp}]); - router.resetConfig([{path: 'a', component: SimpleCmp}]); + router.navigate(['/a']); + advance(fixture); + expect(location.path()).toEqual('/a'); + expect(fixture.nativeElement).toHaveText('simple'); - router.navigate(['/a']); - advance(fixture); - expect(location.path()).toEqual('/a'); - expect(fixture.nativeElement).toHaveText('simple'); + router.resetConfig([{path: 'a', component: RouteCmp}]); - router.resetConfig([{path: 'a', component: RouteCmp}]); + router.navigate(['/a']); + advance(fixture); + expect(location.path()).toEqual('/a'); + expect(fixture.nativeElement).toHaveText('route'); + }), + )); - router.navigate(['/a']); - advance(fixture); - expect(location.path()).toEqual('/a'); - expect(fixture.nativeElement).toHaveText('route'); - }))); + it('should navigate when locations changes', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - it('should navigate when locations changes', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [{path: 'user/:name', component: UserCmp}], + }, + ]); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [{path: 'user/:name', component: UserCmp}] - }]); + const recordedEvents: (NavigationStart | NavigationEnd)[] = []; + router.events.forEach((e) => onlyNavigationStartAndEnd(e) && recordedEvents.push(e)); - const recordedEvents: (NavigationStart|NavigationEnd)[] = []; - router.events.forEach(e => onlyNavigationStartAndEnd(e) && recordedEvents.push(e)); + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); - router.navigateByUrl('/team/22/user/victor'); - advance(fixture); + location.go('/team/22/user/fedor'); + location.historyGo(0); + advance(fixture); - location.go('/team/22/user/fedor'); - location.historyGo(0); - advance(fixture); + location.go('/team/22/user/fedor'); + location.historyGo(0); + advance(fixture); - location.go('/team/22/user/fedor'); - location.historyGo(0); - advance(fixture); + expect(fixture.nativeElement).toHaveText('team 22 [ user fedor, right: ]'); - expect(fixture.nativeElement).toHaveText('team 22 [ user fedor, right: ]'); + expectEvents(recordedEvents, [ + [NavigationStart, '/team/22/user/victor'], + [NavigationEnd, '/team/22/user/victor'], + [NavigationStart, '/team/22/user/fedor'], + [NavigationEnd, '/team/22/user/fedor'], + ]); + }), + )); - expectEvents(recordedEvents, [ - [NavigationStart, '/team/22/user/victor'], [NavigationEnd, '/team/22/user/victor'], - [NavigationStart, '/team/22/user/fedor'], [NavigationEnd, '/team/22/user/fedor'] - ]); - }))); + it('should update the location when the matched route does not change', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - it('should update the location when the matched route does not change', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + router.resetConfig([{path: '**', component: CollectParamsCmp}]); - router.resetConfig([{path: '**', component: CollectParamsCmp}]); + router.navigateByUrl('/one/two'); + advance(fixture); + const cmp = fixture.debugElement.children[1].componentInstance; + expect(location.path()).toEqual('/one/two'); + expect(fixture.nativeElement).toHaveText('collect-params'); - router.navigateByUrl('/one/two'); - advance(fixture); - const cmp = fixture.debugElement.children[1].componentInstance; - expect(location.path()).toEqual('/one/two'); - expect(fixture.nativeElement).toHaveText('collect-params'); + expect(cmp.recordedUrls()).toEqual(['one/two']); - expect(cmp.recordedUrls()).toEqual(['one/two']); - - router.navigateByUrl('/three/four'); - advance(fixture); - expect(location.path()).toEqual('/three/four'); - expect(fixture.nativeElement).toHaveText('collect-params'); - expect(cmp.recordedUrls()).toEqual(['one/two', 'three/four']); - }))); + router.navigateByUrl('/three/four'); + advance(fixture); + expect(location.path()).toEqual('/three/four'); + expect(fixture.nativeElement).toHaveText('collect-params'); + expect(cmp.recordedUrls()).toEqual(['one/two', 'three/four']); + }), + )); describe('duplicate in-flight navigations', () => { @Injectable() @@ -1311,66 +1431,67 @@ for (const browserAPI of ['navigation', 'history'] as const) { provide: 'in1Second', useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { let res: any = null; - const p = new Promise(_ => res = _); + const p = new Promise((_) => (res = _)); setTimeout(() => res(true), 1000); return p; - } + }, }, - RedirectingGuard - ] + RedirectingGuard, + ], }); }); it('should reset location if a navigation by location is successful', fakeAsync(() => { - const router = TestBed.inject(Router); - const location = TestBed.inject(Location); - const fixture = createRoot(router, RootCmp); + const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'simple', component: SimpleCmp, canActivate: ['in1Second']}]); + router.resetConfig([{path: 'simple', component: SimpleCmp, canActivate: ['in1Second']}]); - // Trigger two location changes to the same URL. - // Because of the guard the order will look as follows: - // - location change 'simple' - // - start processing the change, start a guard - // - location change 'simple' - // - the first location change gets canceled, the URL gets reset to '/' - // - the second location change gets finished, the URL should be reset to '/simple' - location.go('/simple'); - location.historyGo(0); - location.historyGo(0); + // Trigger two location changes to the same URL. + // Because of the guard the order will look as follows: + // - location change 'simple' + // - start processing the change, start a guard + // - location change 'simple' + // - the first location change gets canceled, the URL gets reset to '/' + // - the second location change gets finished, the URL should be reset to '/simple' + location.go('/simple'); + location.historyGo(0); + location.historyGo(0); - tick(2000); - advance(fixture); + tick(2000); + advance(fixture); - expect(location.path()).toEqual('/simple'); - })); + expect(location.path()).toEqual('/simple'); + })); it('should skip duplicate location events', fakeAsync(() => { - const router = TestBed.inject(Router); - const location = TestBed.inject(Location); - const fixture = createRoot(router, RootCmp); + const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - { - path: 'blocked', - component: BlankCmp, - canActivate: [() => coreInject(RedirectingGuard).canActivate()] - }, - {path: 'simple', component: SimpleCmp} - ]); - router.navigateByUrl('/simple'); - advance(fixture); + router.resetConfig([ + { + path: 'blocked', + component: BlankCmp, + canActivate: [() => coreInject(RedirectingGuard).canActivate()], + }, + {path: 'simple', component: SimpleCmp}, + ]); + router.navigateByUrl('/simple'); + advance(fixture); - location.go('/blocked'); - location.historyGo(0); + location.go('/blocked'); + location.historyGo(0); - advance(fixture); - expect(fixture.nativeElement.innerHTML).toContain('simple'); - })); + advance(fixture); + expect(fixture.nativeElement.innerHTML).toContain('simple'); + })); it('should not cause URL thrashing', async () => { - TestBed.configureTestingModule( - {providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))]}); + TestBed.configureTestingModule({ + providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))], + }); const router = TestBed.inject(Router); const location = TestBed.inject(Location); @@ -1378,17 +1499,18 @@ for (const browserAPI of ['navigation', 'history'] as const) { fixture.detectChanges(); router.resetConfig([ - {path: 'home', component: SimpleCmp}, { + {path: 'home', component: SimpleCmp}, + { path: 'blocked', component: BlankCmp, - canActivate: [() => coreInject(RedirectingGuard).canActivate()] + canActivate: [() => coreInject(RedirectingGuard).canActivate()], }, - {path: 'simple', component: SimpleCmp} + {path: 'simple', component: SimpleCmp}, ]); await router.navigateByUrl('/home'); const urlChanges: string[] = []; - location.onUrlChange(change => { + location.onUrlChange((change) => { urlChanges.push(change); }); @@ -1401,361 +1523,401 @@ for (const browserAPI of ['navigation', 'history'] as const) { }); it('can render a 404 page without changing the URL', fakeAsync(() => { - TestBed.configureTestingModule( - {providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))]}); - const router = TestBed.inject(Router); - TestBed.inject(RedirectingGuard).skipLocationChange = true; - const location = TestBed.inject(Location); - const fixture = createRoot(router, RootCmp); + TestBed.configureTestingModule({ + providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))], + }); + const router = TestBed.inject(Router); + TestBed.inject(RedirectingGuard).skipLocationChange = true; + const location = TestBed.inject(Location); + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'home', component: SimpleCmp}, - { - path: 'blocked', - component: BlankCmp, - canActivate: [() => coreInject(RedirectingGuard).canActivate()] - }, - {path: 'simple', redirectTo: '404'}, - {path: '404', component: SimpleCmp}, - ]); - router.navigateByUrl('/home'); - advance(fixture); + router.resetConfig([ + {path: 'home', component: SimpleCmp}, + { + path: 'blocked', + component: BlankCmp, + canActivate: [() => coreInject(RedirectingGuard).canActivate()], + }, + {path: 'simple', redirectTo: '404'}, + {path: '404', component: SimpleCmp}, + ]); + router.navigateByUrl('/home'); + advance(fixture); - location.go('/blocked'); - location.historyGo(0); - advance(fixture); - expect(location.path()).toEqual('/blocked'); - expect(fixture.nativeElement.innerHTML).toContain('simple'); - })); + location.go('/blocked'); + location.historyGo(0); + advance(fixture); + expect(location.path()).toEqual('/blocked'); + expect(fixture.nativeElement.innerHTML).toContain('simple'); + })); it('should accurately track currentNavigation', fakeAsync(() => { - const router = TestBed.inject(Router); - router.resetConfig([ - {path: 'one', component: SimpleCmp, canActivate: ['in1Second']}, - {path: 'two', component: BlankCmp, canActivate: ['in1Second']}, - ]); + const router = TestBed.inject(Router); + router.resetConfig([ + {path: 'one', component: SimpleCmp, canActivate: ['in1Second']}, + {path: 'two', component: BlankCmp, canActivate: ['in1Second']}, + ]); - router.events.subscribe((e) => { - if (e instanceof NavigationStart) { - if (e.url === '/one') { - router.navigateByUrl('two'); - } - router.events.subscribe((e) => { - if (e instanceof GuardsCheckEnd) { - expect(router.getCurrentNavigation()?.extractedUrl.toString()).toEqual('/two'); - expect(router.getCurrentNavigation()?.extras).toBeDefined(); - } - }); - } - }); + router.events.subscribe((e) => { + if (e instanceof NavigationStart) { + if (e.url === '/one') { + router.navigateByUrl('two'); + } + router.events.subscribe((e) => { + if (e instanceof GuardsCheckEnd) { + expect(router.getCurrentNavigation()?.extractedUrl.toString()).toEqual('/two'); + expect(router.getCurrentNavigation()?.extras).toBeDefined(); + } + }); + } + }); - router.navigateByUrl('one'); - tick(1000); - })); + router.navigateByUrl('one'); + tick(1000); + })); }); - it('should support secondary routes', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'user/:name', component: UserCmp}, - {path: 'simple', component: SimpleCmp, outlet: 'right'} - ] - }]); - - router.navigateByUrl('/team/22/(user/victor//right:simple)'); - advance(fixture); - - expect(fixture.nativeElement).toHaveText('team 22 [ user victor, right: simple ]'); - }))); - - it('should support secondary routes in separate commands', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'user/:name', component: UserCmp}, - {path: 'simple', component: SimpleCmp, outlet: 'right'} - ] - }]); - - router.navigateByUrl('/team/22/user/victor'); - advance(fixture); - router.navigate(['team/22', {outlets: {right: 'simple'}}]); - advance(fixture); - - expect(fixture.nativeElement).toHaveText('team 22 [ user victor, right: simple ]'); - }))); - - it('should support secondary routes as child of empty path parent', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); - - router.resetConfig([{ - path: '', - component: TeamCmp, - children: [{path: 'simple', component: SimpleCmp, outlet: 'right'}] - }]); - - router.navigateByUrl('/(right:simple)'); - advance(fixture); - - expect(fixture.nativeElement).toHaveText('team [ , right: simple ]'); - }))); - - it('should deactivate outlets', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'user/:name', component: UserCmp}, - {path: 'simple', component: SimpleCmp, outlet: 'right'} - ] - }]); - - router.navigateByUrl('/team/22/(user/victor//right:simple)'); - advance(fixture); - - router.navigateByUrl('/team/22/user/victor'); - advance(fixture); - - expect(fixture.nativeElement).toHaveText('team 22 [ user victor, right: ]'); - }))); - - it('should deactivate nested outlets', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); - - router.resetConfig([ - { - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'user/:name', component: UserCmp}, - {path: 'simple', component: SimpleCmp, outlet: 'right'} - ] - }, - {path: '', component: BlankCmp} - ]); - - router.navigateByUrl('/team/22/(user/victor//right:simple)'); - advance(fixture); - - router.navigateByUrl('/'); - advance(fixture); - - expect(fixture.nativeElement).toHaveText(''); - }))); - - it('should set query params and fragment', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); - - router.resetConfig([{path: 'query', component: QueryParamsAndFragmentCmp}]); - - router.navigateByUrl('/query?name=1#fragment1'); - advance(fixture); - expect(fixture.nativeElement).toHaveText('query: 1 fragment: fragment1'); - - router.navigateByUrl('/query?name=2#fragment2'); - advance(fixture); - expect(fixture.nativeElement).toHaveText('query: 2 fragment: fragment2'); - }))); - - it('should handle empty or missing fragments', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); - - router.resetConfig([{path: 'query', component: QueryParamsAndFragmentCmp}]); - - router.navigateByUrl('/query#'); - advance(fixture); - expect(fixture.nativeElement).toHaveText('query: fragment: '); - - router.navigateByUrl('/query'); - advance(fixture); - expect(fixture.nativeElement).toHaveText('query: fragment: null'); - }))); - - it('should ignore null and undefined query params', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); - - router.resetConfig([{path: 'query', component: EmptyQueryParamsCmp}]); - - router.navigate(['query'], {queryParams: {name: 1, age: null, page: undefined}}); - advance(fixture); - const cmp = fixture.debugElement.children[1].componentInstance; - expect(cmp.recordedParams).toEqual([{name: '1'}]); - }))); - - it('should throw an error when one of the commands is null/undefined', - fakeAsync(inject([Router], (router: Router) => { - createRoot(router, RootCmp); - - router.resetConfig([{path: 'query', component: EmptyQueryParamsCmp}]); - - expect(() => router.navigate([ - undefined, 'query' - ])).toThrowError(/The requested path contains undefined segment at index 0/); - }))); - - it('should push params only when they change', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); - - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [{path: 'user/:name', component: UserCmp}] - }]); - - router.navigateByUrl('/team/22/user/victor'); - advance(fixture); - const team = fixture.debugElement.children[1].componentInstance; - const user = fixture.debugElement.children[1].children[1].componentInstance; - - expect(team.recordedParams).toEqual([{id: '22'}]); - expect(team.snapshotParams).toEqual([{id: '22'}]); - expect(user.recordedParams).toEqual([{name: 'victor'}]); - expect(user.snapshotParams).toEqual([{name: 'victor'}]); - - router.navigateByUrl('/team/22/user/fedor'); - advance(fixture); - - expect(team.recordedParams).toEqual([{id: '22'}]); - expect(team.snapshotParams).toEqual([{id: '22'}]); - expect(user.recordedParams).toEqual([{name: 'victor'}, {name: 'fedor'}]); - expect(user.snapshotParams).toEqual([{name: 'victor'}, {name: 'fedor'}]); - }))); - - it('should work when navigating to /', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); - - router.resetConfig([ - {path: '', pathMatch: 'full', component: SimpleCmp}, - {path: 'user/:name', component: UserCmp} - ]); - - router.navigateByUrl('/user/victor'); - advance(fixture); - - expect(fixture.nativeElement).toHaveText('user victor'); - - router.navigateByUrl('/'); - advance(fixture); - - expect(fixture.nativeElement).toHaveText('simple'); - }))); - - it('should cancel in-flight navigations', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); - - router.resetConfig([{path: 'user/:name', component: UserCmp}]); - - const recordedEvents: Event[] = []; - router.events.forEach(e => recordedEvents.push(e)); - - router.navigateByUrl('/user/init'); - advance(fixture); - - const user = fixture.debugElement.children[1].componentInstance; - - let r1: any, r2: any; - router.navigateByUrl('/user/victor').then(_ => r1 = _); - router.navigateByUrl('/user/fedor').then(_ => r2 = _); - advance(fixture); - - expect(r1).toEqual(false); // returns false because it was canceled - expect(r2).toEqual(true); // returns true because it was successful - - expect(fixture.nativeElement).toHaveText('user fedor'); - expect(user.recordedParams).toEqual([{name: 'init'}, {name: 'fedor'}]); - - expectEvents(recordedEvents, [ - [NavigationStart, '/user/init'], - [RoutesRecognized, '/user/init'], - [GuardsCheckStart, '/user/init'], - [ChildActivationStart], - [ActivationStart], - [GuardsCheckEnd, '/user/init'], - [ResolveStart, '/user/init'], - [ResolveEnd, '/user/init'], - [ActivationEnd], - [ChildActivationEnd], - [NavigationEnd, '/user/init'], - - [NavigationStart, '/user/victor'], - [NavigationCancel, '/user/victor'], - - [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 properly set currentNavigation when cancelling in-flight navigations', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); - - router.resetConfig([{path: 'user/:name', component: UserCmp}]); - - router.navigateByUrl('/user/init'); - advance(fixture); - - router.navigateByUrl('/user/victor'); - expect(router.getCurrentNavigation()).not.toBe(null); - router.navigateByUrl('/user/fedor'); - // Due to https://github.com/angular/angular/issues/29389, this would be `false` - // when running a second navigation. - expect(router.getCurrentNavigation()).not.toBe(null); - advance(fixture); - - expect(router.getCurrentNavigation()).toBe(null); - expect(fixture.nativeElement).toHaveText('user fedor'); - }))); - - it('should handle failed navigations gracefully', - fakeAsync(inject([Router], (router: Router) => { - const fixture = 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 = _); - advance(fixture); - expect(e.message).toContain('Cannot match any routes'); - - router.navigateByUrl('/user/fedor'); - 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 support secondary routes', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); + + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'user/:name', component: UserCmp}, + {path: 'simple', component: SimpleCmp, outlet: 'right'}, + ], + }, + ]); + + router.navigateByUrl('/team/22/(user/victor//right:simple)'); + advance(fixture); + + expect(fixture.nativeElement).toHaveText('team 22 [ user victor, right: simple ]'); + }), + )); + + it('should support secondary routes in separate commands', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); + + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'user/:name', component: UserCmp}, + {path: 'simple', component: SimpleCmp, outlet: 'right'}, + ], + }, + ]); + + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); + router.navigate(['team/22', {outlets: {right: 'simple'}}]); + advance(fixture); + + expect(fixture.nativeElement).toHaveText('team 22 [ user victor, right: simple ]'); + }), + )); + + it('should support secondary routes as child of empty path parent', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); + + router.resetConfig([ + { + path: '', + component: TeamCmp, + children: [{path: 'simple', component: SimpleCmp, outlet: 'right'}], + }, + ]); + + router.navigateByUrl('/(right:simple)'); + advance(fixture); + + expect(fixture.nativeElement).toHaveText('team [ , right: simple ]'); + }), + )); + + it('should deactivate outlets', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); + + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'user/:name', component: UserCmp}, + {path: 'simple', component: SimpleCmp, outlet: 'right'}, + ], + }, + ]); + + router.navigateByUrl('/team/22/(user/victor//right:simple)'); + advance(fixture); + + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); + + expect(fixture.nativeElement).toHaveText('team 22 [ user victor, right: ]'); + }), + )); + + it('should deactivate nested outlets', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); + + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'user/:name', component: UserCmp}, + {path: 'simple', component: SimpleCmp, outlet: 'right'}, + ], + }, + {path: '', component: BlankCmp}, + ]); + + router.navigateByUrl('/team/22/(user/victor//right:simple)'); + advance(fixture); + + router.navigateByUrl('/'); + advance(fixture); + + expect(fixture.nativeElement).toHaveText(''); + }), + )); + + it('should set query params and fragment', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); + + router.resetConfig([{path: 'query', component: QueryParamsAndFragmentCmp}]); + + router.navigateByUrl('/query?name=1#fragment1'); + advance(fixture); + expect(fixture.nativeElement).toHaveText('query: 1 fragment: fragment1'); + + router.navigateByUrl('/query?name=2#fragment2'); + advance(fixture); + expect(fixture.nativeElement).toHaveText('query: 2 fragment: fragment2'); + }), + )); + + it('should handle empty or missing fragments', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); + + router.resetConfig([{path: 'query', component: QueryParamsAndFragmentCmp}]); + + router.navigateByUrl('/query#'); + advance(fixture); + expect(fixture.nativeElement).toHaveText('query: fragment: '); + + router.navigateByUrl('/query'); + advance(fixture); + expect(fixture.nativeElement).toHaveText('query: fragment: null'); + }), + )); + + it('should ignore null and undefined query params', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); + + router.resetConfig([{path: 'query', component: EmptyQueryParamsCmp}]); + + router.navigate(['query'], {queryParams: {name: 1, age: null, page: undefined}}); + advance(fixture); + const cmp = fixture.debugElement.children[1].componentInstance; + expect(cmp.recordedParams).toEqual([{name: '1'}]); + }), + )); + + it('should throw an error when one of the commands is null/undefined', fakeAsync( + inject([Router], (router: Router) => { + createRoot(router, RootCmp); + + router.resetConfig([{path: 'query', component: EmptyQueryParamsCmp}]); + + expect(() => router.navigate([undefined, 'query'])).toThrowError( + /The requested path contains undefined segment at index 0/, + ); + }), + )); + + it('should push params only when they change', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); + + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [{path: 'user/:name', component: UserCmp}], + }, + ]); + + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); + const team = fixture.debugElement.children[1].componentInstance; + const user = fixture.debugElement.children[1].children[1].componentInstance; + + expect(team.recordedParams).toEqual([{id: '22'}]); + expect(team.snapshotParams).toEqual([{id: '22'}]); + expect(user.recordedParams).toEqual([{name: 'victor'}]); + expect(user.snapshotParams).toEqual([{name: 'victor'}]); + + router.navigateByUrl('/team/22/user/fedor'); + advance(fixture); + + expect(team.recordedParams).toEqual([{id: '22'}]); + expect(team.snapshotParams).toEqual([{id: '22'}]); + expect(user.recordedParams).toEqual([{name: 'victor'}, {name: 'fedor'}]); + expect(user.snapshotParams).toEqual([{name: 'victor'}, {name: 'fedor'}]); + }), + )); + + it('should work when navigating to /', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); + + router.resetConfig([ + {path: '', pathMatch: 'full', component: SimpleCmp}, + {path: 'user/:name', component: UserCmp}, + ]); + + router.navigateByUrl('/user/victor'); + advance(fixture); + + expect(fixture.nativeElement).toHaveText('user victor'); + + router.navigateByUrl('/'); + advance(fixture); + + expect(fixture.nativeElement).toHaveText('simple'); + }), + )); + + it('should cancel in-flight navigations', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); + + router.resetConfig([{path: 'user/:name', component: UserCmp}]); + + const recordedEvents: Event[] = []; + router.events.forEach((e) => recordedEvents.push(e)); + + router.navigateByUrl('/user/init'); + advance(fixture); + + const user = fixture.debugElement.children[1].componentInstance; + + let r1: any, r2: any; + router.navigateByUrl('/user/victor').then((_) => (r1 = _)); + router.navigateByUrl('/user/fedor').then((_) => (r2 = _)); + advance(fixture); + + expect(r1).toEqual(false); // returns false because it was canceled + expect(r2).toEqual(true); // returns true because it was successful + + expect(fixture.nativeElement).toHaveText('user fedor'); + expect(user.recordedParams).toEqual([{name: 'init'}, {name: 'fedor'}]); + + expectEvents(recordedEvents, [ + [NavigationStart, '/user/init'], + [RoutesRecognized, '/user/init'], + [GuardsCheckStart, '/user/init'], + [ChildActivationStart], + [ActivationStart], + [GuardsCheckEnd, '/user/init'], + [ResolveStart, '/user/init'], + [ResolveEnd, '/user/init'], + [ActivationEnd], + [ChildActivationEnd], + [NavigationEnd, '/user/init'], + + [NavigationStart, '/user/victor'], + [NavigationCancel, '/user/victor'], + + [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 properly set currentNavigation when cancelling in-flight navigations', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); + + router.resetConfig([{path: 'user/:name', component: UserCmp}]); + + router.navigateByUrl('/user/init'); + advance(fixture); + + router.navigateByUrl('/user/victor'); + expect(router.getCurrentNavigation()).not.toBe(null); + router.navigateByUrl('/user/fedor'); + // Due to https://github.com/angular/angular/issues/29389, this would be `false` + // when running a second navigation. + expect(router.getCurrentNavigation()).not.toBe(null); + advance(fixture); + + expect(router.getCurrentNavigation()).toBe(null); + expect(fixture.nativeElement).toHaveText('user fedor'); + }), + )); + + it('should handle failed navigations gracefully', fakeAsync( + inject([Router], (router: Router) => { + const fixture = 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 = _)); + advance(fixture); + expect(e.message).toContain('Cannot match any routes'); + + router.navigateByUrl('/user/fedor'); + 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'}) @@ -1765,15 +1927,20 @@ for (const browserAPI of ['navigation', 'history'] as const) { TestBed.configureTestingModule({ providers: [ provideRouter( - [{ + [ + { path: 'throw', - canMatch: [() => { - throw new Error(''); - }], - component: BlankCmp - }], - withNavigationErrorHandler(() => coreInject(Handler).handlerCalled = true)), - ] + canMatch: [ + () => { + throw new Error(''); + }, + ], + component: BlankCmp, + }, + ], + withNavigationErrorHandler(() => (coreInject(Handler).handlerCalled = true)), + ), + ], }); const router = TestBed.inject(Router); await expectAsync(router.navigateByUrl('/throw')).toBeRejected(); @@ -1781,206 +1948,221 @@ for (const browserAPI of ['navigation', 'history'] as const) { }); // Errors should behave the same for both deferred and eager URL update strategies - (['deferred', 'eager'] as const).forEach(urlUpdateStrategy => { + (['deferred', 'eager'] as const).forEach((urlUpdateStrategy) => { it('should dispatch NavigationError after the url has been reset back', fakeAsync(() => { - TestBed.configureTestingModule( - {providers: [provideRouter([], withRouterConfig({urlUpdateStrategy}))]}); - const router: Router = TestBed.inject(Router); - const location = TestBed.inject(Location); - const fixture = createRoot(router, RootCmp); + TestBed.configureTestingModule({ + providers: [provideRouter([], withRouterConfig({urlUpdateStrategy}))], + }); + const router: Router = TestBed.inject(Router); + const location = TestBed.inject(Location); + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'simple', component: SimpleCmp}, {path: 'throwing', component: ThrowingCmp} - ]); + router.resetConfig([ + {path: 'simple', component: SimpleCmp}, + {path: 'throwing', component: ThrowingCmp}, + ]); - router.navigateByUrl('/simple'); - advance(fixture); + router.navigateByUrl('/simple'); + 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); - 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); + advance(fixture); - expect(routerUrlBeforeEmittingError).toEqual('/simple'); - expect(locationUrlBeforeEmittingError).toEqual('/simple'); - })); + expect(routerUrlBeforeEmittingError).toEqual('/simple'); + expect(locationUrlBeforeEmittingError).toEqual('/simple'); + })); it('can renavigate to throwing component', fakeAsync(() => { - 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 = createRoot(router, RootCmp); + 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 = createRoot(router, RootCmp); - // Try navigating to a component which throws an error during activation. - ConditionalThrowingCmp.throwError = true; - expect(() => { - router.navigateByUrl('/throwing'); - advance(fixture); - }).toThrow(); - expect(location.path()).toEqual(''); - expect(fixture.nativeElement.innerHTML).not.toContain('throwing'); + // Try navigating to a component which throws an error during activation. + ConditionalThrowingCmp.throwError = true; + expect(() => { + router.navigateByUrl('/throwing'); + advance(fixture); + }).toThrow(); + 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'); - advance(fixture); - expect(location.path()).toEqual('/throwing'); - expect(fixture.nativeElement.innerHTML).toContain('throwing'); - })); + // Ensure we can re-navigate to that same URL and succeed. + ConditionalThrowingCmp.throwError = false; + router.navigateByUrl('/throwing'); + 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', fakeAsync(() => { - TestBed.configureTestingModule( - {providers: [provideRouter([], withRouterConfig({urlUpdateStrategy}))]}); - const router: Router = TestBed.inject(Router); - const location = TestBed.inject(Location); - const fixture = createRoot(router, RootCmp); + TestBed.configureTestingModule({ + providers: [provideRouter([], withRouterConfig({urlUpdateStrategy}))], + }); + const router: Router = TestBed.inject(Router); + const location = TestBed.inject(Location); + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'simple1', component: SimpleCmp}, {path: 'simple2', component: SimpleCmp}, - {path: 'throwing', component: ThrowingCmp} - ]); + 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; - } - }); + let event: NavigationStart; + router.events.subscribe((e) => { + if (e instanceof NavigationStart) { + event = e; + } + }); - router.navigateByUrl('/simple1'); - advance(fixture); - const simple1NavStart = event!; + router.navigateByUrl('/simple1'); + advance(fixture); + const simple1NavStart = event!; - router.navigateByUrl('/throwing').catch(() => null); - advance(fixture); + router.navigateByUrl('/throwing').catch(() => null); + advance(fixture); - router.navigateByUrl('/simple2'); - advance(fixture); + router.navigateByUrl('/simple2'); + advance(fixture); - location.back(); - tick(); + location.back(); + tick(); - expect(event!.restoredState!.navigationId).toEqual(simple1NavStart.id); - })); + expect(event!.restoredState!.navigationId).toEqual(simple1NavStart.id); + })); - it('should not trigger another navigation when resetting the url back due to a NavigationError', - fakeAsync(() => { - TestBed.configureTestingModule( - {providers: [provideRouter([], withRouterConfig({urlUpdateStrategy}))]}); - const router = TestBed.inject(Router); - router.onSameUrlNavigation = 'reload'; + it('should not trigger another navigation when resetting the url back due to a NavigationError', fakeAsync(() => { + TestBed.configureTestingModule({ + providers: [provideRouter([], withRouterConfig({urlUpdateStrategy}))], + }); + const router = TestBed.inject(Router); + router.onSameUrlNavigation = 'reload'; - const fixture = createRoot(router, RootCmp); + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'simple', component: SimpleCmp}, {path: 'throwing', component: ThrowingCmp} - ]); + 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); - } - }); + const events: any[] = []; + router.events.forEach((e: any) => { + if (e instanceof NavigationStart) { + events.push(e.url); + } + }); - router.navigateByUrl('/simple'); - advance(fixture); + router.navigateByUrl('/simple'); + advance(fixture); - router.navigateByUrl('/throwing').catch(() => null); - advance(fixture); + router.navigateByUrl('/throwing').catch(() => null); + advance(fixture); - // we do not trigger another navigation to /simple - expect(events).toEqual(['/simple', '/throwing']); - })); + // we do not trigger another navigation to /simple + expect(events).toEqual(['/simple', '/throwing']); + })); }); it('should dispatch NavigationCancel after the url has been reset back', fakeAsync(() => { - TestBed.configureTestingModule( - {providers: [{provide: 'returnsFalse', useValue: () => false}]}); + TestBed.configureTestingModule({ + providers: [{provide: 'returnsFalse', useValue: () => false}], + }); - const router: Router = TestBed.inject(Router); - const location = TestBed.inject(Location); + const router: Router = TestBed.inject(Router); + const location = TestBed.inject(Location); - const fixture = createRoot(router, RootCmp); + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'simple', component: SimpleCmp}, { - path: 'throwing', - loadChildren: jasmine.createSpy('doesnotmatter'), - canLoad: ['returnsFalse'] - } - ]); + router.resetConfig([ + {path: 'simple', component: SimpleCmp}, + { + path: 'throwing', + loadChildren: jasmine.createSpy('doesnotmatter'), + canLoad: ['returnsFalse'], + }, + ]); - router.navigateByUrl('/simple'); - advance(fixture); + router.navigateByUrl('/simple'); + 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(); - } - }); + let routerUrlBeforeEmittingError = ''; + let locationUrlBeforeEmittingError = ''; + router.events.forEach((e) => { + if (e instanceof NavigationCancel) { + expect(e.code).toBe(NavigationCancellationCode.GuardRejected); + routerUrlBeforeEmittingError = router.url; + locationUrlBeforeEmittingError = location.path(); + } + }); - location.go('/throwing'); - location.historyGo(0); - advance(fixture); + location.go('/throwing'); + location.historyGo(0); + advance(fixture); - expect(routerUrlBeforeEmittingError).toEqual('/simple'); - expect(locationUrlBeforeEmittingError).toEqual('/simple'); - })); + expect(routerUrlBeforeEmittingError).toEqual('/simple'); + expect(locationUrlBeforeEmittingError).toEqual('/simple'); + })); - it('should support custom error handlers', fakeAsync(inject([Router], (router: Router) => { - router.errorHandler = (error) => 'resolvedValue'; - const fixture = createRoot(router, RootCmp); + it('should support custom error handlers', fakeAsync( + inject([Router], (router: Router) => { + router.errorHandler = (error) => 'resolvedValue'; + const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'user/:name', component: UserCmp}]); + router.resetConfig([{path: 'user/:name', component: UserCmp}]); - const recordedEvents: any[] = []; - router.events.forEach(e => recordedEvents.push(e)); + const recordedEvents: any[] = []; + router.events.forEach((e) => recordedEvents.push(e)); - let e: any; - router.navigateByUrl('/invalid')!.then(_ => e = _); - advance(fixture); - expect(e).toEqual('resolvedValue'); + let e: any; + router.navigateByUrl('/invalid')!.then((_) => (e = _)); + advance(fixture); + expect(e).toEqual('resolvedValue'); - expectEvents( - recordedEvents, [[NavigationStart, '/invalid'], [NavigationError, '/invalid']]); - }))); + expectEvents(recordedEvents, [ + [NavigationStart, '/invalid'], + [NavigationError, '/invalid'], + ]); + }), + )); - it('should recover from malformed uri errors', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - router.resetConfig([{path: 'simple', component: SimpleCmp}]); - const fixture = createRoot(router, RootCmp); - router.navigateByUrl('/invalid/url%with%percent'); - advance(fixture); - expect(location.path()).toEqual(''); - }))); + it('should recover from malformed uri errors', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + router.resetConfig([{path: 'simple', component: SimpleCmp}]); + const fixture = createRoot(router, RootCmp); + router.navigateByUrl('/invalid/url%with%percent'); + advance(fixture); + expect(location.path()).toEqual(''); + }), + )); - it('should not swallow errors', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should not swallow errors', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'simple', component: SimpleCmp}]); + router.resetConfig([{path: 'simple', component: SimpleCmp}]); - router.navigateByUrl('/invalid'); - expect(() => advance(fixture)).toThrow(); + router.navigateByUrl('/invalid'); + expect(() => advance(fixture)).toThrow(); - router.navigateByUrl('/invalid2'); - expect(() => advance(fixture)).toThrow(); - }))); + router.navigateByUrl('/invalid2'); + expect(() => advance(fixture)).toThrow(); + }), + )); it('should not swallow errors from browser state update', async () => { const routerEvents: Event[] = []; @@ -1993,8 +2175,7 @@ for (const browserAPI of ['navigation', 'history'] as const) { }); try { await RouterTestingHarness.create('/abc123'); - } catch { - } + } 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); @@ -2003,157 +2184,163 @@ for (const browserAPI of ['navigation', 'history'] as const) { expect(routerEvents[routerEvents.length - 1]).toBeInstanceOf(NavigationError); }); - it('should replace state when path is equal to current path', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should replace state when path is equal to current path', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: - [{path: 'simple', component: SimpleCmp}, {path: 'user/:name', component: UserCmp}] - }]); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'simple', component: SimpleCmp}, + {path: 'user/:name', component: UserCmp}, + ], + }, + ]); - router.navigateByUrl('/team/33/simple'); - advance(fixture); + router.navigateByUrl('/team/33/simple'); + advance(fixture); - router.navigateByUrl('/team/22/user/victor'); - advance(fixture); + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); - router.navigateByUrl('/team/22/user/victor'); - advance(fixture); + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); - location.back(); - advance(fixture); - expect(location.path()).toEqual('/team/33/simple'); - }))); + location.back(); + advance(fixture); + expect(location.path()).toEqual('/team/33/simple'); + }), + )); - it('should handle componentless paths', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmpWithTwoOutlets); + it('should handle componentless paths', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmpWithTwoOutlets); - router.resetConfig([ - { - path: 'parent/:id', - children: [ - {path: 'simple', component: SimpleCmp}, - {path: 'user/:name', component: UserCmp, outlet: 'right'} - ] - }, - {path: 'user/:name', component: UserCmp} - ]); + router.resetConfig([ + { + path: 'parent/:id', + children: [ + {path: 'simple', component: SimpleCmp}, + {path: 'user/:name', component: UserCmp, outlet: 'right'}, + ], + }, + {path: 'user/:name', component: UserCmp}, + ]); + // navigate to a componentless route + router.navigateByUrl('/parent/11/(simple//right:user/victor)'); + advance(fixture); + expect(location.path()).toEqual('/parent/11/(simple//right:user/victor)'); + expect(fixture.nativeElement).toHaveText('primary [simple] right [user victor]'); - // navigate to a componentless route - router.navigateByUrl('/parent/11/(simple//right:user/victor)'); - advance(fixture); - expect(location.path()).toEqual('/parent/11/(simple//right:user/victor)'); - expect(fixture.nativeElement).toHaveText('primary [simple] right [user victor]'); + // navigate to the same route with different params (reuse) + router.navigateByUrl('/parent/22/(simple//right:user/fedor)'); + advance(fixture); + expect(location.path()).toEqual('/parent/22/(simple//right:user/fedor)'); + expect(fixture.nativeElement).toHaveText('primary [simple] right [user fedor]'); - // navigate to the same route with different params (reuse) - router.navigateByUrl('/parent/22/(simple//right:user/fedor)'); - advance(fixture); - expect(location.path()).toEqual('/parent/22/(simple//right:user/fedor)'); - expect(fixture.nativeElement).toHaveText('primary [simple] right [user fedor]'); + // navigate to a normal route (check deactivation) + router.navigateByUrl('/user/victor'); + advance(fixture); + expect(location.path()).toEqual('/user/victor'); + expect(fixture.nativeElement).toHaveText('primary [user victor] right []'); - // navigate to a normal route (check deactivation) - router.navigateByUrl('/user/victor'); - advance(fixture); - expect(location.path()).toEqual('/user/victor'); - expect(fixture.nativeElement).toHaveText('primary [user victor] right []'); + // navigate back to a componentless route + router.navigateByUrl('/parent/11/(simple//right:user/victor)'); + advance(fixture); + expect(location.path()).toEqual('/parent/11/(simple//right:user/victor)'); + expect(fixture.nativeElement).toHaveText('primary [simple] right [user victor]'); + }), + )); - // navigate back to a componentless route - router.navigateByUrl('/parent/11/(simple//right:user/victor)'); - advance(fixture); - expect(location.path()).toEqual('/parent/11/(simple//right:user/victor)'); - expect(fixture.nativeElement).toHaveText('primary [simple] right [user victor]'); - }))); + it('should not deactivate aux routes when navigating from a componentless routes', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, TwoOutletsCmp); - it('should not deactivate aux routes when navigating from a componentless routes', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, TwoOutletsCmp); + router.resetConfig([ + {path: 'simple', component: SimpleCmp}, + {path: 'componentless', children: [{path: 'simple', component: SimpleCmp}]}, + {path: 'user/:name', outlet: 'aux', component: UserCmp}, + ]); - router.resetConfig([ - {path: 'simple', component: SimpleCmp}, - {path: 'componentless', children: [{path: 'simple', component: SimpleCmp}]}, - {path: 'user/:name', outlet: 'aux', component: UserCmp} - ]); + router.navigateByUrl('/componentless/simple(aux:user/victor)'); + advance(fixture); + expect(location.path()).toEqual('/componentless/simple(aux:user/victor)'); + expect(fixture.nativeElement).toHaveText('[ simple, aux: user victor ]'); - router.navigateByUrl('/componentless/simple(aux:user/victor)'); - advance(fixture); - expect(location.path()).toEqual('/componentless/simple(aux:user/victor)'); - expect(fixture.nativeElement).toHaveText('[ simple, aux: user victor ]'); - - router.navigateByUrl('/simple(aux:user/victor)'); - advance(fixture); - expect(location.path()).toEqual('/simple(aux:user/victor)'); - expect(fixture.nativeElement).toHaveText('[ simple, aux: user victor ]'); - }))); + router.navigateByUrl('/simple(aux:user/victor)'); + advance(fixture); + expect(location.path()).toEqual('/simple(aux:user/victor)'); + expect(fixture.nativeElement).toHaveText('[ simple, aux: user victor ]'); + }), + )); it('should emit an event when an outlet gets activated', fakeAsync(() => { - @Component({ - selector: 'container', - template: - `` - }) - class Container { - activations: any[] = []; - deactivations: any[] = []; + @Component({ + selector: 'container', + template: ``, + }) + class Container { + activations: any[] = []; + deactivations: any[] = []; - recordActivate(component: any): void { - this.activations.push(component); - } + recordActivate(component: any): void { + this.activations.push(component); + } - recordDeactivate(component: any): void { - this.deactivations.push(component); - } - } + recordDeactivate(component: any): void { + this.deactivations.push(component); + } + } - TestBed.configureTestingModule({declarations: [Container]}); + TestBed.configureTestingModule({declarations: [Container]}); - const router: Router = TestBed.inject(Router); + const router: Router = TestBed.inject(Router); - const fixture = createRoot(router, Container); - const cmp = fixture.componentInstance; + const fixture = createRoot(router, Container); + const cmp = fixture.componentInstance; - router.resetConfig( - [{path: 'blank', component: BlankCmp}, {path: 'simple', component: SimpleCmp}]); + router.resetConfig([ + {path: 'blank', component: BlankCmp}, + {path: 'simple', component: SimpleCmp}, + ]); - cmp.activations = []; - cmp.deactivations = []; + cmp.activations = []; + cmp.deactivations = []; - router.navigateByUrl('/blank'); - advance(fixture); + router.navigateByUrl('/blank'); + advance(fixture); - expect(cmp.activations.length).toEqual(1); - expect(cmp.activations[0] instanceof BlankCmp).toBe(true); + expect(cmp.activations.length).toEqual(1); + expect(cmp.activations[0] instanceof BlankCmp).toBe(true); - router.navigateByUrl('/simple'); - advance(fixture); + router.navigateByUrl('/simple'); + advance(fixture); - expect(cmp.activations.length).toEqual(2); - expect(cmp.activations[1] instanceof SimpleCmp).toBe(true); - expect(cmp.deactivations.length).toEqual(1); - expect(cmp.deactivations[0] instanceof BlankCmp).toBe(true); - })); + expect(cmp.activations.length).toEqual(2); + expect(cmp.activations[1] instanceof SimpleCmp).toBe(true); + expect(cmp.deactivations.length).toEqual(1); + expect(cmp.deactivations[0] instanceof BlankCmp).toBe(true); + })); - it('should update url and router state before activating components', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should update url and router state before activating components', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'cmp', component: ComponentRecordingRoutePathAndUrl}]); + router.resetConfig([{path: 'cmp', component: ComponentRecordingRoutePathAndUrl}]); - router.navigateByUrl('/cmp'); - advance(fixture); - - const cmp: ComponentRecordingRoutePathAndUrl = - fixture.debugElement.children[1].componentInstance; - - expect(cmp.url).toBe('/cmp'); - expect(cmp.path.length).toEqual(2); - }))); + router.navigateByUrl('/cmp'); + advance(fixture); + const cmp: ComponentRecordingRoutePathAndUrl = + fixture.debugElement.children[1].componentInstance; + expect(cmp.url).toBe('/cmp'); + expect(cmp.path.length).toEqual(2); + }), + )); describe('data', () => { class ResolveSix { @@ -2166,7 +2353,7 @@ for (const browserAPI of ['navigation', 'history'] as const) { class NestedComponentWithData { data: any = []; constructor(private route: ActivatedRoute) { - route.data.forEach(d => this.data.push(d)); + route.data.forEach((d) => this.data.push(d)); } } @@ -2185,234 +2372,253 @@ for (const browserAPI of ['navigation', 'history'] as const) { useValue: (route: ActivatedRouteSnapshot) => { route.data = {prop: 10}; return true; - } + }, }, - ] + ], }); }); - it('should provide resolved data', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmpWithTwoOutlets); + it('should provide resolved data', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmpWithTwoOutlets); - router.resetConfig([{ - path: 'parent/:id', - data: {one: 1}, - resolve: {two: 'resolveTwo'}, - children: [ - {path: '', data: {three: 3}, resolve: {four: 'resolveFour'}, component: RouteCmp}, - { - path: '', - data: {five: 5}, - resolve: {six: 'resolveSix'}, - component: RouteCmp, - outlet: 'right' - }, - ] - }]); + router.resetConfig([ + { + path: 'parent/:id', + data: {one: 1}, + resolve: {two: 'resolveTwo'}, + children: [ + {path: '', data: {three: 3}, resolve: {four: 'resolveFour'}, component: RouteCmp}, + { + path: '', + data: {five: 5}, + resolve: {six: 'resolveSix'}, + component: RouteCmp, + outlet: 'right', + }, + ], + }, + ]); - router.navigateByUrl('/parent/1'); - advance(fixture); + router.navigateByUrl('/parent/1'); + advance(fixture); - const primaryCmp = fixture.debugElement.children[1].componentInstance; - const rightCmp = fixture.debugElement.children[3].componentInstance; + const primaryCmp = fixture.debugElement.children[1].componentInstance; + const rightCmp = fixture.debugElement.children[3].componentInstance; - expect(primaryCmp.route.snapshot.data).toEqual({one: 1, two: 2, three: 3, four: 4}); - expect(rightCmp.route.snapshot.data).toEqual({one: 1, two: 2, five: 5, six: 6}); + expect(primaryCmp.route.snapshot.data).toEqual({one: 1, two: 2, three: 3, four: 4}); + expect(rightCmp.route.snapshot.data).toEqual({one: 1, two: 2, five: 5, six: 6}); - const primaryRecorded: any[] = []; - primaryCmp.route.data.forEach((rec: any) => primaryRecorded.push(rec)); + const primaryRecorded: any[] = []; + primaryCmp.route.data.forEach((rec: any) => primaryRecorded.push(rec)); - const rightRecorded: any[] = []; - rightCmp.route.data.forEach((rec: any) => rightRecorded.push(rec)); + const rightRecorded: any[] = []; + rightCmp.route.data.forEach((rec: any) => rightRecorded.push(rec)); - router.navigateByUrl('/parent/2'); - advance(fixture); + router.navigateByUrl('/parent/2'); + advance(fixture); - expect(primaryRecorded).toEqual([{one: 1, three: 3, two: 2, four: 4}]); - expect(rightRecorded).toEqual([{one: 1, five: 5, two: 2, six: 6}]); - }))); + expect(primaryRecorded).toEqual([{one: 1, three: 3, two: 2, four: 4}]); + expect(rightRecorded).toEqual([{one: 1, five: 5, two: 2, six: 6}]); + }), + )); - it('should handle errors', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should handle errors', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig( - [{path: 'simple', component: SimpleCmp, resolve: {error: 'resolveError'}}]); + router.resetConfig([ + {path: 'simple', component: SimpleCmp, resolve: {error: 'resolveError'}}, + ]); - const recordedEvents: any[] = []; - router.events.subscribe(e => e instanceof RouterEvent && recordedEvents.push(e)); + const recordedEvents: any[] = []; + router.events.subscribe((e) => e instanceof RouterEvent && recordedEvents.push(e)); - let e: any = null; - router.navigateByUrl('/simple')!.catch(error => e = error); - advance(fixture); + let e: any = null; + router.navigateByUrl('/simple')!.catch((error) => (e = error)); + advance(fixture); - expectEvents(recordedEvents, [ - [NavigationStart, '/simple'], [RoutesRecognized, '/simple'], - [GuardsCheckStart, '/simple'], [GuardsCheckEnd, '/simple'], [ResolveStart, '/simple'], - [NavigationError, '/simple'] - ]); + expectEvents(recordedEvents, [ + [NavigationStart, '/simple'], + [RoutesRecognized, '/simple'], + [GuardsCheckStart, '/simple'], + [GuardsCheckEnd, '/simple'], + [ResolveStart, '/simple'], + [NavigationError, '/simple'], + ]); - expect(e).toEqual('error'); - }))); + expect(e).toEqual('error'); + }), + )); - it('should handle empty errors', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should handle empty errors', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig( - [{path: 'simple', component: SimpleCmp, resolve: {error: 'resolveNullError'}}]); + router.resetConfig([ + {path: 'simple', component: SimpleCmp, resolve: {error: 'resolveNullError'}}, + ]); - const recordedEvents: any[] = []; - router.events.subscribe(e => e instanceof RouterEvent && recordedEvents.push(e)); + const recordedEvents: any[] = []; + router.events.subscribe((e) => e instanceof RouterEvent && recordedEvents.push(e)); - let e: any = 'some value'; - router.navigateByUrl('/simple').catch(error => e = error); - advance(fixture); + let e: any = 'some value'; + router.navigateByUrl('/simple').catch((error) => (e = error)); + advance(fixture); - expect(e).toEqual(null); - }))); + expect(e).toEqual(null); + }), + )); - it('should not navigate when all resolvers return empty result', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should not navigate when all resolvers return empty result', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'simple', - component: SimpleCmp, - resolve: {e1: 'resolveEmpty', e2: 'resolveEmpty'} - }]); + router.resetConfig([ + { + path: 'simple', + component: SimpleCmp, + resolve: {e1: 'resolveEmpty', e2: 'resolveEmpty'}, + }, + ]); - const recordedEvents: any[] = []; - router.events.subscribe(e => e instanceof RouterEvent && recordedEvents.push(e)); + const recordedEvents: any[] = []; + router.events.subscribe((e) => e instanceof RouterEvent && recordedEvents.push(e)); - let e: any = null; - router.navigateByUrl('/simple').catch(error => e = error); - advance(fixture); + let e: any = null; + router.navigateByUrl('/simple').catch((error) => (e = error)); + advance(fixture); - expectEvents(recordedEvents, [ - [NavigationStart, '/simple'], - [RoutesRecognized, '/simple'], - [GuardsCheckStart, '/simple'], - [GuardsCheckEnd, '/simple'], - [ResolveStart, '/simple'], - [NavigationCancel, '/simple'], - ]); + expectEvents(recordedEvents, [ + [NavigationStart, '/simple'], + [RoutesRecognized, '/simple'], + [GuardsCheckStart, '/simple'], + [GuardsCheckEnd, '/simple'], + [ResolveStart, '/simple'], + [NavigationCancel, '/simple'], + ]); - expect((recordedEvents[recordedEvents.length - 1] as NavigationCancel).code) - .toBe(NavigationCancellationCode.NoDataFromResolver); + expect((recordedEvents[recordedEvents.length - 1] as NavigationCancel).code).toBe( + NavigationCancellationCode.NoDataFromResolver, + ); - expect(e).toEqual(null); - }))); + expect(e).toEqual(null); + }), + )); - it('should not navigate when at least one resolver returns empty result', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should not navigate when at least one resolver returns empty result', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'simple', component: SimpleCmp, resolve: {e1: 'resolveTwo', e2: 'resolveEmpty'}} - ]); + router.resetConfig([ + {path: 'simple', component: SimpleCmp, resolve: {e1: 'resolveTwo', e2: 'resolveEmpty'}}, + ]); - const recordedEvents: any[] = []; - router.events.subscribe(e => e instanceof RouterEvent && recordedEvents.push(e)); + const recordedEvents: any[] = []; + router.events.subscribe((e) => e instanceof RouterEvent && recordedEvents.push(e)); - let e: any = null; - router.navigateByUrl('/simple').catch(error => e = error); - advance(fixture); + let e: any = null; + router.navigateByUrl('/simple').catch((error) => (e = error)); + advance(fixture); - expectEvents(recordedEvents, [ - [NavigationStart, '/simple'], - [RoutesRecognized, '/simple'], - [GuardsCheckStart, '/simple'], - [GuardsCheckEnd, '/simple'], - [ResolveStart, '/simple'], - [NavigationCancel, '/simple'], - ]); + expectEvents(recordedEvents, [ + [NavigationStart, '/simple'], + [RoutesRecognized, '/simple'], + [GuardsCheckStart, '/simple'], + [GuardsCheckEnd, '/simple'], + [ResolveStart, '/simple'], + [NavigationCancel, '/simple'], + ]); - expect(e).toEqual(null); - }))); + expect(e).toEqual(null); + }), + )); - it('should not navigate when all resolvers for a child route from forChild() returns empty result', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should not navigate when all resolvers for a child route from forChild() returns empty result', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - @Component({selector: 'lazy-cmp', template: 'lazy-loaded-1'}) - class LazyComponent1 { - } + @Component({selector: 'lazy-cmp', template: 'lazy-loaded-1'}) + class LazyComponent1 {} + @NgModule({ + declarations: [LazyComponent1], + imports: [ + RouterModule.forChild([ + { + path: 'loaded', + component: LazyComponent1, + resolve: {e1: 'resolveEmpty', e2: 'resolveEmpty'}, + }, + ]), + ], + }) + class LoadedModule {} - @NgModule({ - declarations: [LazyComponent1], - imports: [ - RouterModule.forChild([{ - path: 'loaded', - component: LazyComponent1, - resolve: {e1: 'resolveEmpty', e2: 'resolveEmpty'} - }]), - ], - }) - class LoadedModule { - } + router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); - router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); + const recordedEvents: any[] = []; + router.events.subscribe((e) => e instanceof RouterEvent && recordedEvents.push(e)); - const recordedEvents: any[] = []; - router.events.subscribe(e => e instanceof RouterEvent && recordedEvents.push(e)); + let e: any = null; + router.navigateByUrl('lazy/loaded').catch((error) => (e = error)); + advance(fixture); - let e: any = null; - router.navigateByUrl('lazy/loaded').catch(error => e = error); - advance(fixture); + expectEvents(recordedEvents, [ + [NavigationStart, '/lazy/loaded'], + [RoutesRecognized, '/lazy/loaded'], + [GuardsCheckStart, '/lazy/loaded'], + [GuardsCheckEnd, '/lazy/loaded'], + [ResolveStart, '/lazy/loaded'], + [NavigationCancel, '/lazy/loaded'], + ]); - expectEvents(recordedEvents, [ - [NavigationStart, '/lazy/loaded'], - [RoutesRecognized, '/lazy/loaded'], - [GuardsCheckStart, '/lazy/loaded'], - [GuardsCheckEnd, '/lazy/loaded'], - [ResolveStart, '/lazy/loaded'], - [NavigationCancel, '/lazy/loaded'], - ]); + expect(e).toEqual(null); + }), + )); - expect(e).toEqual(null); - }))); + it('should not navigate when at least one resolver for a child route from forChild() returns empty result', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - it('should not navigate when at least one resolver for a child route from forChild() returns empty result', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + @Component({selector: 'lazy-cmp', template: 'lazy-loaded-1'}) + class LazyComponent1 {} - @Component({selector: 'lazy-cmp', template: 'lazy-loaded-1'}) - class LazyComponent1 { - } + @NgModule({ + declarations: [LazyComponent1], + imports: [ + RouterModule.forChild([ + { + path: 'loaded', + component: LazyComponent1, + resolve: {e1: 'resolveTwo', e2: 'resolveEmpty'}, + }, + ]), + ], + }) + class LoadedModule {} - @NgModule({ - declarations: [LazyComponent1], - imports: [ - RouterModule.forChild([{ - path: 'loaded', - component: LazyComponent1, - resolve: {e1: 'resolveTwo', e2: 'resolveEmpty'} - }]), - ], - }) - class LoadedModule { - } + router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); - router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); + const recordedEvents: any[] = []; + router.events.subscribe((e) => e instanceof RouterEvent && recordedEvents.push(e)); - const recordedEvents: any[] = []; - router.events.subscribe(e => e instanceof RouterEvent && recordedEvents.push(e)); + let e: any = null; + router.navigateByUrl('lazy/loaded').catch((error) => (e = error)); + advance(fixture); - let e: any = null; - router.navigateByUrl('lazy/loaded').catch(error => e = error); - advance(fixture); + expectEvents(recordedEvents, [ + [NavigationStart, '/lazy/loaded'], + [RoutesRecognized, '/lazy/loaded'], + [GuardsCheckStart, '/lazy/loaded'], + [GuardsCheckEnd, '/lazy/loaded'], + [ResolveStart, '/lazy/loaded'], + [NavigationCancel, '/lazy/loaded'], + ]); - expectEvents(recordedEvents, [ - [NavigationStart, '/lazy/loaded'], - [RoutesRecognized, '/lazy/loaded'], - [GuardsCheckStart, '/lazy/loaded'], - [GuardsCheckEnd, '/lazy/loaded'], - [ResolveStart, '/lazy/loaded'], - [NavigationCancel, '/lazy/loaded'], - ]); - - expect(e).toEqual(null); - }))); + expect(e).toEqual(null); + }), + )); it('should include target snapshot in NavigationError when resolver throws', async () => { const router = TestBed.inject(Router); @@ -2424,14 +2630,15 @@ for (const browserAPI of ['navigation', 'history'] as const) { } } - let caughtError: NavigationError|undefined; - router.events.subscribe(e => { + let caughtError: NavigationError | undefined; + router.events.subscribe((e) => { if (e instanceof NavigationError) { caughtError = e; } }); - router.resetConfig( - [{path: 'throwing', resolve: {thrower: ThrowingResolver}, component: BlankCmp}]); + router.resetConfig([ + {path: 'throwing', resolve: {thrower: ThrowingResolver}, component: BlankCmp}, + ]); try { await router.navigateByUrl('/throwing'); fail('navigation should throw'); @@ -2443,249 +2650,277 @@ for (const browserAPI of ['navigation', 'history'] as const) { expect(caughtError?.target).toBeDefined(); }); - it('should preserve resolved data', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should preserve resolved data', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'parent', - resolve: {two: 'resolveTwo'}, - children: [ - {path: 'child1', component: CollectParamsCmp}, - {path: 'child2', component: CollectParamsCmp} - ] - }]); + router.resetConfig([ + { + path: 'parent', + resolve: {two: 'resolveTwo'}, + children: [ + {path: 'child1', component: CollectParamsCmp}, + {path: 'child2', component: CollectParamsCmp}, + ], + }, + ]); - router.navigateByUrl('/parent/child1'); - advance(fixture); + router.navigateByUrl('/parent/child1'); + advance(fixture); - router.navigateByUrl('/parent/child2'); - advance(fixture); + router.navigateByUrl('/parent/child2'); + advance(fixture); - const cmp: CollectParamsCmp = fixture.debugElement.children[1].componentInstance; - expect(cmp.route.snapshot.data).toEqual({two: 2}); - }))); + const cmp: CollectParamsCmp = fixture.debugElement.children[1].componentInstance; + expect(cmp.route.snapshot.data).toEqual({two: 2}); + }), + )); - it('should override route static data with resolved data', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should override route static data with resolved data', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: '', - component: NestedComponentWithData, - resolve: {prop: 'resolveTwo'}, - data: {prop: 'static'}, - }]); + router.resetConfig([ + { + path: '', + component: NestedComponentWithData, + resolve: {prop: 'resolveTwo'}, + data: {prop: 'static'}, + }, + ]); - router.navigateByUrl('/'); - advance(fixture); - const cmp = fixture.debugElement.children[1].componentInstance; + router.navigateByUrl('/'); + advance(fixture); + const cmp = fixture.debugElement.children[1].componentInstance; - expect(cmp.data).toEqual([{prop: 2}]); - }))); + expect(cmp.data).toEqual([{prop: 2}]); + }), + )); - it('should correctly override inherited route static data with resolved data', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should correctly override inherited route static data with resolved data', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'a', - component: WrapperCmp, - resolve: {prop2: 'resolveTwo'}, - data: {prop: 'wrapper-a'}, - children: [ - // will inherit data from this child route because it has `path` and its parent has - // component - { - path: 'b', - data: {prop: 'nested-b'}, - resolve: {prop3: 'resolveFour'}, - children: [ - { - path: 'c', - children: - [{path: '', component: NestedComponentWithData, data: {prop3: 'nested'}}] - }, - ] - }, - ], - }]); + router.resetConfig([ + { + path: 'a', + component: WrapperCmp, + resolve: {prop2: 'resolveTwo'}, + data: {prop: 'wrapper-a'}, + children: [ + // will inherit data from this child route because it has `path` and its parent has + // component + { + path: 'b', + data: {prop: 'nested-b'}, + resolve: {prop3: 'resolveFour'}, + children: [ + { + path: 'c', + children: [ + {path: '', component: NestedComponentWithData, data: {prop3: 'nested'}}, + ], + }, + ], + }, + ], + }, + ]); - router.navigateByUrl('/a/b/c'); - advance(fixture); + router.navigateByUrl('/a/b/c'); + advance(fixture); - const pInj = - fixture.debugElement.queryAll(By.directive(NestedComponentWithData))[0].injector!; - const cmp = pInj.get(NestedComponentWithData); - expect(cmp.data).toEqual([{prop: 'nested-b', prop3: 'nested'}]); - }))); + const pInj = fixture.debugElement.queryAll(By.directive(NestedComponentWithData))[0] + .injector!; + const cmp = pInj.get(NestedComponentWithData); + expect(cmp.data).toEqual([{prop: 'nested-b', prop3: 'nested'}]); + }), + )); - it('should not override inherited resolved data with inherited static data', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should not override inherited resolved data with inherited static data', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'a', - component: WrapperCmp, - resolve: {prop2: 'resolveTwo'}, - data: {prop: 'wrapper-a'}, - children: [ - // will inherit data from this child route because it has `path` and its parent has - // component - { - path: 'b', - data: {prop2: 'parent-b', prop: 'parent-b'}, - children: [ - { - path: 'c', - resolve: {prop2: 'resolveFour'}, - children: [ - { - path: '', - component: NestedComponentWithData, - data: {prop: 'nested-d'}, - }, - ] - }, - ] - }, - ], - }]); + router.resetConfig([ + { + path: 'a', + component: WrapperCmp, + resolve: {prop2: 'resolveTwo'}, + data: {prop: 'wrapper-a'}, + children: [ + // will inherit data from this child route because it has `path` and its parent has + // component + { + path: 'b', + data: {prop2: 'parent-b', prop: 'parent-b'}, + children: [ + { + path: 'c', + resolve: {prop2: 'resolveFour'}, + children: [ + { + path: '', + component: NestedComponentWithData, + data: {prop: 'nested-d'}, + }, + ], + }, + ], + }, + ], + }, + ]); - router.navigateByUrl('/a/b/c'); - advance(fixture); + router.navigateByUrl('/a/b/c'); + advance(fixture); - const pInj = - fixture.debugElement.queryAll(By.directive(NestedComponentWithData))[0].injector!; - const cmp = pInj.get(NestedComponentWithData); - expect(cmp.data).toEqual([{prop: 'nested-d', prop2: 4}]); - }))); + const pInj = fixture.debugElement.queryAll(By.directive(NestedComponentWithData))[0] + .injector!; + const cmp = pInj.get(NestedComponentWithData); + expect(cmp.data).toEqual([{prop: 'nested-d', prop2: 4}]); + }), + )); - it('should not override nested route static data when both are using resolvers', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should not override nested route static data when both are using resolvers', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - { - path: 'child', - component: WrapperCmp, - resolve: {prop: 'resolveTwo'}, - children: [{ - path: '', - pathMatch: 'full', - component: NestedComponentWithData, - resolve: {prop: 'resolveFour'} - }] - }, - ]); + router.resetConfig([ + { + path: 'child', + component: WrapperCmp, + resolve: {prop: 'resolveTwo'}, + children: [ + { + path: '', + pathMatch: 'full', + component: NestedComponentWithData, + resolve: {prop: 'resolveFour'}, + }, + ], + }, + ]); - router.navigateByUrl('/child'); - advance(fixture); + router.navigateByUrl('/child'); + advance(fixture); - const pInj = fixture.debugElement.query(By.directive(NestedComponentWithData)).injector!; - const cmp = pInj.get(NestedComponentWithData); - expect(cmp.data).toEqual([{prop: 4}]); - }))); + const pInj = fixture.debugElement.query(By.directive(NestedComponentWithData)).injector!; + const cmp = pInj.get(NestedComponentWithData); + expect(cmp.data).toEqual([{prop: 4}]); + }), + )); - it('should not override child route\'s static data when both are using static data', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it("should not override child route's static data when both are using static data", fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - { - path: 'child', - component: WrapperCmp, - data: {prop: 'wrapper'}, - children: [{ - path: '', - pathMatch: 'full', - component: NestedComponentWithData, - data: {prop: 'inner'} - }] - }, - ]); + router.resetConfig([ + { + path: 'child', + component: WrapperCmp, + data: {prop: 'wrapper'}, + children: [ + { + path: '', + pathMatch: 'full', + component: NestedComponentWithData, + data: {prop: 'inner'}, + }, + ], + }, + ]); - router.navigateByUrl('/child'); - advance(fixture); + router.navigateByUrl('/child'); + advance(fixture); - const pInj = fixture.debugElement.query(By.directive(NestedComponentWithData)).injector!; - const cmp = pInj.get(NestedComponentWithData); - expect(cmp.data).toEqual([{prop: 'inner'}]); - }))); + const pInj = fixture.debugElement.query(By.directive(NestedComponentWithData)).injector!; + const cmp = pInj.get(NestedComponentWithData); + expect(cmp.data).toEqual([{prop: 'inner'}]); + }), + )); - it('should not override child route\'s static data when wrapper is using resolved data and the child route static data', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it("should not override child route's static data when wrapper is using resolved data and the child route static data", fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - { - path: 'nested', - component: WrapperCmp, - resolve: {prop: 'resolveTwo', prop2: 'resolveSix'}, - data: {prop3: 'wrapper-static', prop4: 'another-static'}, - children: [{ - path: '', - pathMatch: 'full', - component: NestedComponentWithData, - data: {prop: 'nested', prop4: 'nested-static'} - }] - }, - ]); + router.resetConfig([ + { + path: 'nested', + component: WrapperCmp, + resolve: {prop: 'resolveTwo', prop2: 'resolveSix'}, + data: {prop3: 'wrapper-static', prop4: 'another-static'}, + children: [ + { + path: '', + pathMatch: 'full', + component: NestedComponentWithData, + data: {prop: 'nested', prop4: 'nested-static'}, + }, + ], + }, + ]); - router.navigateByUrl('/nested'); - advance(fixture); + router.navigateByUrl('/nested'); + advance(fixture); - const pInj = fixture.debugElement.query(By.directive(NestedComponentWithData)).injector!; - const cmp = pInj.get(NestedComponentWithData); - // Issue 34361 - `prop` should contain value defined in `data` object from the nested - // route. - expect(cmp.data).toEqual( - [{prop: 'nested', prop2: 6, prop3: 'wrapper-static', prop4: 'nested-static'}]); - }))); + const pInj = fixture.debugElement.query(By.directive(NestedComponentWithData)).injector!; + const cmp = pInj.get(NestedComponentWithData); + // Issue 34361 - `prop` should contain value defined in `data` object from the nested + // route. + expect(cmp.data).toEqual([ + {prop: 'nested', prop2: 6, prop3: 'wrapper-static', prop4: 'nested-static'}, + ]); + }), + )); - it('should allow guards alter data resolved by routes', - fakeAsync(inject([Router], (router: Router) => { - // This is not documented or recommended behavior but is here to prevent unexpected - // regressions. This behavior isn't necessary 'by design' but it was discovered during a - // refactor that some teams depend on it. - const fixture = createRoot(router, RootCmp); + it('should allow guards alter data resolved by routes', fakeAsync( + inject([Router], (router: Router) => { + // This is not documented or recommended behavior but is here to prevent unexpected + // regressions. This behavior isn't necessary 'by design' but it was discovered during a + // refactor that some teams depend on it. + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - { - path: 'route', - component: NestedComponentWithData, - canActivate: ['overridingGuard'], - }, - ]); + router.resetConfig([ + { + path: 'route', + component: NestedComponentWithData, + canActivate: ['overridingGuard'], + }, + ]); - router.navigateByUrl('/route'); - advance(fixture); + router.navigateByUrl('/route'); + advance(fixture); - const pInj = fixture.debugElement.query(By.directive(NestedComponentWithData)).injector!; - const cmp = pInj.get(NestedComponentWithData); - expect(cmp.data).toEqual([{prop: 10}]); - }))); + const pInj = fixture.debugElement.query(By.directive(NestedComponentWithData)).injector!; + const cmp = pInj.get(NestedComponentWithData); + expect(cmp.data).toEqual([{prop: 10}]); + }), + )); - it('should rerun resolvers when the urls segments of a wildcard route change', - fakeAsync(inject([Router, Location], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should rerun resolvers when the urls segments of a wildcard route change', fakeAsync( + inject([Router, Location], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: '**', - component: CollectParamsCmp, - resolve: {numberOfUrlSegments: 'numberOfUrlSegments'} - }]); + router.resetConfig([ + { + path: '**', + component: CollectParamsCmp, + resolve: {numberOfUrlSegments: 'numberOfUrlSegments'}, + }, + ]); - router.navigateByUrl('/one/two'); - advance(fixture); - const cmp = fixture.debugElement.children[1].componentInstance; + router.navigateByUrl('/one/two'); + advance(fixture); + const cmp = fixture.debugElement.children[1].componentInstance; - expect(cmp.route.snapshot.data).toEqual({numberOfUrlSegments: 2}); + expect(cmp.route.snapshot.data).toEqual({numberOfUrlSegments: 2}); - router.navigateByUrl('/one/two/three'); - advance(fixture); + router.navigateByUrl('/one/two/three'); + advance(fixture); - expect(cmp.route.snapshot.data).toEqual({numberOfUrlSegments: 3}); - }))); + expect(cmp.route.snapshot.data).toEqual({numberOfUrlSegments: 3}); + }), + )); describe('should run resolvers for the same route concurrently', () => { let log: string[]; @@ -2703,424 +2938,449 @@ for (const browserAPI of ['navigation', 'history'] as const) { return () => {}; }); return obs$.pipe(map(() => log.push('resolver1'))); - } + }, }, { provide: 'resolver2', useValue: () => { - return of(null).pipe(map(() => { - log.push('resolver2'); - observer.next(null); - observer.complete(); - })); - } + return of(null).pipe( + map(() => { + log.push('resolver2'); + observer.next(null); + observer.complete(); + }), + ); + }, }, - ] + ], }); }); - it('works', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('works', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'a', - resolve: { - one: 'resolver1', - two: 'resolver2', - }, - component: SimpleCmp - }]); + router.resetConfig([ + { + path: 'a', + resolve: { + one: 'resolver1', + two: 'resolver2', + }, + component: SimpleCmp, + }, + ]); - router.navigateByUrl('/a'); - advance(fixture); + router.navigateByUrl('/a'); + advance(fixture); - expect(log).toEqual(['resolver2', 'resolver1']); - }))); + expect(log).toEqual(['resolver2', 'resolver1']); + }), + )); }); it('can resolve symbol keys', fakeAsync(() => { - const router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmp); - const symbolKey = Symbol('key'); + const router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmp); + const symbolKey = Symbol('key'); - router.resetConfig( - [{path: 'simple', component: SimpleCmp, resolve: {[symbolKey]: 'resolveFour'}}]); + router.resetConfig([ + {path: 'simple', component: SimpleCmp, resolve: {[symbolKey]: 'resolveFour'}}, + ]); - router.navigateByUrl('/simple'); - advance(fixture); + router.navigateByUrl('/simple'); + advance(fixture); - expect(router.routerState.root.snapshot.firstChild!.data[symbolKey]).toEqual(4); - })); + expect(router.routerState.root.snapshot.firstChild!.data[symbolKey]).toEqual(4); + })); it('should allow resolvers as pure functions', fakeAsync(() => { - const router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmp); - const user = Symbol('user'); + const router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmp); + const user = Symbol('user'); - const userResolver: ResolveFn = (route: ActivatedRouteSnapshot) => - route.params['user']; - router.resetConfig( - [{path: ':user', component: SimpleCmp, resolve: {[user]: userResolver}}]); + const userResolver: ResolveFn = (route: ActivatedRouteSnapshot) => + route.params['user']; + router.resetConfig([ + {path: ':user', component: SimpleCmp, resolve: {[user]: userResolver}}, + ]); - router.navigateByUrl('/atscott'); - advance(fixture); + router.navigateByUrl('/atscott'); + advance(fixture); - expect(router.routerState.root.snapshot.firstChild!.data[user]).toEqual('atscott'); - })); + expect(router.routerState.root.snapshot.firstChild!.data[user]).toEqual('atscott'); + })); it('should allow DI in resolvers as pure functions', fakeAsync(() => { - const router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmp); - const user = Symbol('user'); + const router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmp); + const user = Symbol('user'); - @Injectable({providedIn: 'root'}) - class LoginState { - user = 'atscott'; - } + @Injectable({providedIn: 'root'}) + class LoginState { + user = 'atscott'; + } - router.resetConfig([{ - path: '**', - component: SimpleCmp, - resolve: { - [user]: () => coreInject(LoginState).user, - }, - }]); + router.resetConfig([ + { + path: '**', + component: SimpleCmp, + resolve: { + [user]: () => coreInject(LoginState).user, + }, + }, + ]); - router.navigateByUrl('/'); - advance(fixture); + router.navigateByUrl('/'); + advance(fixture); - expect(router.routerState.root.snapshot.firstChild!.data[user]).toEqual('atscott'); - })); + expect(router.routerState.root.snapshot.firstChild!.data[user]).toEqual('atscott'); + })); }); describe('router links', () => { - it('should support skipping location update for anchor router links', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = TestBed.createComponent(RootCmp); - advance(fixture); + it('should support skipping location update for anchor router links', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = TestBed.createComponent(RootCmp); + advance(fixture); - router.resetConfig([{path: 'team/:id', component: TeamCmp}]); + router.resetConfig([{path: 'team/:id', component: TeamCmp}]); - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); - expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]'); + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); + expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]'); - const teamCmp = fixture.debugElement.childNodes[1].componentInstance; + const teamCmp = fixture.debugElement.childNodes[1].componentInstance; - teamCmp.routerLink = ['/team/0']; - advance(fixture); - const anchor = fixture.debugElement.query(By.css('a')).nativeElement; - anchor.click(); - advance(fixture); - expect(fixture.nativeElement).toHaveText('team 0 [ , right: ]'); - expect(location.path()).toEqual('/team/22'); + teamCmp.routerLink = ['/team/0']; + advance(fixture); + const anchor = fixture.debugElement.query(By.css('a')).nativeElement; + anchor.click(); + advance(fixture); + expect(fixture.nativeElement).toHaveText('team 0 [ , right: ]'); + expect(location.path()).toEqual('/team/22'); - teamCmp.routerLink = ['/team/1']; - advance(fixture); - const button = fixture.debugElement.query(By.css('button')).nativeElement; - button.click(); - advance(fixture); - expect(fixture.nativeElement).toHaveText('team 1 [ , right: ]'); - expect(location.path()).toEqual('/team/22'); - }))); + teamCmp.routerLink = ['/team/1']; + advance(fixture); + const button = fixture.debugElement.query(By.css('button')).nativeElement; + button.click(); + advance(fixture); + expect(fixture.nativeElement).toHaveText('team 1 [ , right: ]'); + expect(location.path()).toEqual('/team/22'); + }), + )); - it('should support string router links', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should support string router links', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: - [{path: 'link', component: StringLinkCmp}, {path: 'simple', component: SimpleCmp}] - }]); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'link', component: StringLinkCmp}, + {path: 'simple', component: SimpleCmp}, + ], + }, + ]); - router.navigateByUrl('/team/22/link'); - advance(fixture); - expect(fixture.nativeElement).toHaveText('team 22 [ link, right: ]'); + router.navigateByUrl('/team/22/link'); + advance(fixture); + expect(fixture.nativeElement).toHaveText('team 22 [ link, right: ]'); - const native = fixture.nativeElement.querySelector('a'); - expect(native.getAttribute('href')).toEqual('/team/33/simple'); - expect(native.getAttribute('target')).toEqual('_self'); - native.click(); - advance(fixture); + const native = fixture.nativeElement.querySelector('a'); + expect(native.getAttribute('href')).toEqual('/team/33/simple'); + expect(native.getAttribute('target')).toEqual('_self'); + native.click(); + advance(fixture); - expect(fixture.nativeElement).toHaveText('team 33 [ simple, right: ]'); - }))); + expect(fixture.nativeElement).toHaveText('team 33 [ simple, right: ]'); + }), + )); it('should not preserve query params and fragment by default', fakeAsync(() => { - @Component({ - selector: 'someRoot', - template: `Link` - }) - class RootCmpWithLink { - } + @Component({ + selector: 'someRoot', + template: `Link`, + }) + class RootCmpWithLink {} - TestBed.configureTestingModule({declarations: [RootCmpWithLink]}); - const router: Router = TestBed.inject(Router); + TestBed.configureTestingModule({declarations: [RootCmpWithLink]}); + const router: Router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmpWithLink); + const fixture = createRoot(router, RootCmpWithLink); - router.resetConfig([{path: 'home', component: SimpleCmp}]); + router.resetConfig([{path: 'home', component: SimpleCmp}]); - const native = fixture.nativeElement.querySelector('a'); + const native = fixture.nativeElement.querySelector('a'); - router.navigateByUrl('/home?q=123#fragment'); - advance(fixture); - expect(native.getAttribute('href')).toEqual('/home'); - })); + router.navigateByUrl('/home?q=123#fragment'); + advance(fixture); + expect(native.getAttribute('href')).toEqual('/home'); + })); it('should not throw when commands is null or undefined', fakeAsync(() => { - @Component({ - selector: 'someCmp', - template: ` + @Component({ + selector: 'someCmp', + template: ` Link Link - ` - }) - class CmpWithLink { - } + `, + }) + class CmpWithLink {} - TestBed.configureTestingModule({declarations: [CmpWithLink]}); - const router: Router = TestBed.inject(Router); + TestBed.configureTestingModule({declarations: [CmpWithLink]}); + const router: Router = TestBed.inject(Router); - let fixture: ComponentFixture = createRoot(router, CmpWithLink); - router.resetConfig([{path: 'home', component: SimpleCmp}]); - const anchors = fixture.nativeElement.querySelectorAll('a'); - const buttons = fixture.nativeElement.querySelectorAll('button'); - expect(() => anchors[0].click()).not.toThrow(); - expect(() => anchors[1].click()).not.toThrow(); - expect(() => buttons[0].click()).not.toThrow(); - expect(() => buttons[1].click()).not.toThrow(); - })); + let fixture: ComponentFixture = createRoot(router, CmpWithLink); + router.resetConfig([{path: 'home', component: SimpleCmp}]); + const anchors = fixture.nativeElement.querySelectorAll('a'); + const buttons = fixture.nativeElement.querySelectorAll('button'); + expect(() => anchors[0].click()).not.toThrow(); + expect(() => anchors[1].click()).not.toThrow(); + expect(() => buttons[0].click()).not.toThrow(); + expect(() => buttons[1].click()).not.toThrow(); + })); it('should not throw when some command is null', fakeAsync(() => { - @Component({ - selector: 'someCmp', - template: - `Link` - }) - class CmpWithLink { - } + @Component({ + selector: 'someCmp', + template: `Link`, + }) + class CmpWithLink {} - TestBed.configureTestingModule({declarations: [CmpWithLink]}); - const router: Router = TestBed.inject(Router); + TestBed.configureTestingModule({declarations: [CmpWithLink]}); + const router: Router = TestBed.inject(Router); - expect(() => createRoot(router, CmpWithLink)).not.toThrow(); - })); + expect(() => createRoot(router, CmpWithLink)).not.toThrow(); + })); it('should not throw when some command is undefined', fakeAsync(() => { - @Component({ - selector: 'someCmp', - template: - `Link` - }) - class CmpWithLink { - } + @Component({ + selector: 'someCmp', + template: `Link`, + }) + class CmpWithLink {} - TestBed.configureTestingModule({declarations: [CmpWithLink]}); - const router: Router = TestBed.inject(Router); + TestBed.configureTestingModule({declarations: [CmpWithLink]}); + const router: Router = TestBed.inject(Router); - expect(() => createRoot(router, CmpWithLink)).not.toThrow(); - })); + expect(() => createRoot(router, CmpWithLink)).not.toThrow(); + })); it('should update hrefs when query params or fragment change', fakeAsync(() => { - @Component({ - selector: 'someRoot', - template: - `Link` - }) - class RootCmpWithLink { - } - TestBed.configureTestingModule({declarations: [RootCmpWithLink]}); - const router: Router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmpWithLink); + @Component({ + selector: 'someRoot', + template: `Link`, + }) + class RootCmpWithLink {} + TestBed.configureTestingModule({declarations: [RootCmpWithLink]}); + const router: Router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmpWithLink); - router.resetConfig([{path: 'home', component: SimpleCmp}]); + router.resetConfig([{path: 'home', component: SimpleCmp}]); - const native = fixture.nativeElement.querySelector('a'); + const native = fixture.nativeElement.querySelector('a'); - router.navigateByUrl('/home?q=123'); - advance(fixture); - expect(native.getAttribute('href')).toEqual('/home?q=123'); + router.navigateByUrl('/home?q=123'); + advance(fixture); + expect(native.getAttribute('href')).toEqual('/home?q=123'); - router.navigateByUrl('/home?q=456'); - advance(fixture); - expect(native.getAttribute('href')).toEqual('/home?q=456'); + router.navigateByUrl('/home?q=456'); + advance(fixture); + expect(native.getAttribute('href')).toEqual('/home?q=456'); - router.navigateByUrl('/home?q=456#1'); - advance(fixture); - expect(native.getAttribute('href')).toEqual('/home?q=456#1'); - })); + router.navigateByUrl('/home?q=456#1'); + advance(fixture); + expect(native.getAttribute('href')).toEqual('/home?q=456#1'); + })); it('should correctly use the preserve strategy', fakeAsync(() => { - @Component({ - selector: 'someRoot', - template: - `Link` - }) - class RootCmpWithLink { - } - TestBed.configureTestingModule({declarations: [RootCmpWithLink]}); - const router: Router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmpWithLink); + @Component({ + selector: 'someRoot', + template: `Link`, + }) + class RootCmpWithLink {} + TestBed.configureTestingModule({declarations: [RootCmpWithLink]}); + const router: Router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmpWithLink); - router.resetConfig([{path: 'home', component: SimpleCmp}]); + router.resetConfig([{path: 'home', component: SimpleCmp}]); - const native = fixture.nativeElement.querySelector('a'); + const native = fixture.nativeElement.querySelector('a'); - router.navigateByUrl('/home?a=123'); - advance(fixture); - expect(native.getAttribute('href')).toEqual('/home?a=123'); - })); + router.navigateByUrl('/home?a=123'); + advance(fixture); + expect(native.getAttribute('href')).toEqual('/home?a=123'); + })); it('should correctly use the merge strategy', fakeAsync(() => { - @Component({ - selector: 'someRoot', - template: - `Link` - }) - class RootCmpWithLink { - } - TestBed.configureTestingModule({declarations: [RootCmpWithLink]}); - const router: Router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmpWithLink); + @Component({ + selector: 'someRoot', + template: `Link`, + }) + class RootCmpWithLink {} + TestBed.configureTestingModule({declarations: [RootCmpWithLink]}); + const router: Router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmpWithLink); - router.resetConfig([{path: 'home', component: SimpleCmp}]); + router.resetConfig([{path: 'home', component: SimpleCmp}]); - const native = fixture.nativeElement.querySelector('a'); + const native = fixture.nativeElement.querySelector('a'); - router.navigateByUrl('/home?a=123&removeMe=123'); - advance(fixture); - expect(native.getAttribute('href')).toEqual('/home?a=123&q=456'); - })); + router.navigateByUrl('/home?a=123&removeMe=123'); + advance(fixture); + expect(native.getAttribute('href')).toEqual('/home?a=123&q=456'); + })); - it('should support using links on non-a tags', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should support using links on non-a tags', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'link', component: StringLinkButtonCmp}, - {path: 'simple', component: SimpleCmp} - ] - }]); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'link', component: StringLinkButtonCmp}, + {path: 'simple', component: SimpleCmp}, + ], + }, + ]); - router.navigateByUrl('/team/22/link'); - advance(fixture); - expect(fixture.nativeElement).toHaveText('team 22 [ link, right: ]'); + router.navigateByUrl('/team/22/link'); + advance(fixture); + expect(fixture.nativeElement).toHaveText('team 22 [ link, right: ]'); - const button = fixture.nativeElement.querySelector('button'); - expect(button.getAttribute('tabindex')).toEqual('0'); - button.click(); - advance(fixture); + const button = fixture.nativeElement.querySelector('button'); + expect(button.getAttribute('tabindex')).toEqual('0'); + button.click(); + advance(fixture); - expect(fixture.nativeElement).toHaveText('team 33 [ simple, right: ]'); - }))); + expect(fixture.nativeElement).toHaveText('team 33 [ simple, right: ]'); + }), + )); - it('should support absolute router links', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should support absolute router links', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'link', component: AbsoluteLinkCmp}, {path: 'simple', component: SimpleCmp} - ] - }]); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'link', component: AbsoluteLinkCmp}, + {path: 'simple', component: SimpleCmp}, + ], + }, + ]); - router.navigateByUrl('/team/22/link'); - advance(fixture); - expect(fixture.nativeElement).toHaveText('team 22 [ link, right: ]'); + router.navigateByUrl('/team/22/link'); + advance(fixture); + expect(fixture.nativeElement).toHaveText('team 22 [ link, right: ]'); - const native = fixture.nativeElement.querySelector('a'); - expect(native.getAttribute('href')).toEqual('/team/33/simple'); - native.click(); - advance(fixture); + const native = fixture.nativeElement.querySelector('a'); + expect(native.getAttribute('href')).toEqual('/team/33/simple'); + native.click(); + advance(fixture); - expect(fixture.nativeElement).toHaveText('team 33 [ simple, right: ]'); - }))); + expect(fixture.nativeElement).toHaveText('team 33 [ simple, right: ]'); + }), + )); - it('should support relative router links', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should support relative router links', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'link', component: RelativeLinkCmp}, {path: 'simple', component: SimpleCmp} - ] - }]); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'link', component: RelativeLinkCmp}, + {path: 'simple', component: SimpleCmp}, + ], + }, + ]); - router.navigateByUrl('/team/22/link'); - advance(fixture); - expect(fixture.nativeElement).toHaveText('team 22 [ link, right: ]'); + router.navigateByUrl('/team/22/link'); + advance(fixture); + expect(fixture.nativeElement).toHaveText('team 22 [ link, right: ]'); - const native = fixture.nativeElement.querySelector('a'); - expect(native.getAttribute('href')).toEqual('/team/22/simple'); - native.click(); - advance(fixture); + const native = fixture.nativeElement.querySelector('a'); + expect(native.getAttribute('href')).toEqual('/team/22/simple'); + native.click(); + advance(fixture); - expect(fixture.nativeElement).toHaveText('team 22 [ simple, right: ]'); - }))); + expect(fixture.nativeElement).toHaveText('team 22 [ simple, right: ]'); + }), + )); - it('should support top-level link', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RelativeLinkInIfCmp); - advance(fixture); + it('should support top-level link', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RelativeLinkInIfCmp); + advance(fixture); - router.resetConfig( - [{path: 'simple', component: SimpleCmp}, {path: '', component: BlankCmp}]); + router.resetConfig([ + {path: 'simple', component: SimpleCmp}, + {path: '', component: BlankCmp}, + ]); - router.navigateByUrl('/'); - advance(fixture); - expect(fixture.nativeElement).toHaveText(''); - const cmp = fixture.componentInstance; + router.navigateByUrl('/'); + advance(fixture); + expect(fixture.nativeElement).toHaveText(''); + const cmp = fixture.componentInstance; - cmp.show = true; - advance(fixture); + cmp.show = true; + advance(fixture); - expect(fixture.nativeElement).toHaveText('link'); - const native = fixture.nativeElement.querySelector('a'); + expect(fixture.nativeElement).toHaveText('link'); + const native = fixture.nativeElement.querySelector('a'); - expect(native.getAttribute('href')).toEqual('/simple'); - native.click(); - advance(fixture); + expect(native.getAttribute('href')).toEqual('/simple'); + native.click(); + advance(fixture); - expect(fixture.nativeElement).toHaveText('linksimple'); - }))); + expect(fixture.nativeElement).toHaveText('linksimple'); + }), + )); - it('should support query params and fragments', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should support query params and fragments', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'link', component: LinkWithQueryParamsAndFragment}, - {path: 'simple', component: SimpleCmp} - ] - }]); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'link', component: LinkWithQueryParamsAndFragment}, + {path: 'simple', component: SimpleCmp}, + ], + }, + ]); - router.navigateByUrl('/team/22/link'); - advance(fixture); + router.navigateByUrl('/team/22/link'); + advance(fixture); - const native = fixture.nativeElement.querySelector('a'); - expect(native.getAttribute('href')).toEqual('/team/22/simple?q=1#f'); - native.click(); - advance(fixture); + const native = fixture.nativeElement.querySelector('a'); + expect(native.getAttribute('href')).toEqual('/team/22/simple?q=1#f'); + native.click(); + advance(fixture); - expect(fixture.nativeElement).toHaveText('team 22 [ simple, right: ]'); + expect(fixture.nativeElement).toHaveText('team 22 [ simple, right: ]'); - expect(location.path(true)).toEqual('/team/22/simple?q=1#f'); - }))); + expect(location.path(true)).toEqual('/team/22/simple?q=1#f'); + }), + )); describe('should support history and state', () => { - let component: typeof LinkWithState|typeof DivLinkWithState; + let component: typeof LinkWithState | typeof DivLinkWithState; it('for anchor elements', () => { // Test logic in afterEach to reduce duplication component = LinkWithState; @@ -3131,223 +3391,246 @@ for (const browserAPI of ['navigation', 'history'] as const) { component = DivLinkWithState; }); - afterEach(fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + afterEach(fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [{path: 'link', component}, {path: 'simple', component: SimpleCmp}] - }]); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'link', component}, + {path: 'simple', component: SimpleCmp}, + ], + }, + ]); - router.navigateByUrl('/team/22/link'); - advance(fixture); + router.navigateByUrl('/team/22/link'); + advance(fixture); - const native = fixture.nativeElement.querySelector('#link'); - native.click(); - advance(fixture); + const native = fixture.nativeElement.querySelector('#link'); + native.click(); + advance(fixture); - expect(fixture.nativeElement).toHaveText('team 22 [ simple, right: ]'); + expect(fixture.nativeElement).toHaveText('team 22 [ simple, right: ]'); - // Check the history entry - expect(location.getState()).toEqual({foo: 'bar', navigationId: 3}); - }))); + // Check the history entry + expect(location.getState()).toEqual({foo: 'bar', navigationId: 3}); + }), + )); }); it('should set href on area elements', fakeAsync(() => { - @Component({ - selector: 'someRoot', - template: `` - }) - class RootCmpWithArea { - } + @Component({ + selector: 'someRoot', + template: ``, + }) + class RootCmpWithArea {} - TestBed.configureTestingModule({declarations: [RootCmpWithArea]}); - const router: Router = TestBed.inject(Router); + TestBed.configureTestingModule({declarations: [RootCmpWithArea]}); + const router: Router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmpWithArea); + const fixture = createRoot(router, RootCmpWithArea); - router.resetConfig([{path: 'home', component: SimpleCmp}]); + router.resetConfig([{path: 'home', component: SimpleCmp}]); - const native = fixture.nativeElement.querySelector('area'); - expect(native.getAttribute('href')).toEqual('/home'); - })); + const native = fixture.nativeElement.querySelector('area'); + expect(native.getAttribute('href')).toEqual('/home'); + })); }); describe('redirects', () => { - it('should work', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should work', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'old/team/:id', redirectTo: 'team/:id'}, {path: 'team/:id', component: TeamCmp} - ]); + router.resetConfig([ + {path: 'old/team/:id', redirectTo: 'team/:id'}, + {path: 'team/:id', component: TeamCmp}, + ]); - router.navigateByUrl('old/team/22'); - advance(fixture); + router.navigateByUrl('old/team/22'); + advance(fixture); - expect(location.path()).toEqual('/team/22'); - }))); + expect(location.path()).toEqual('/team/22'); + }), + )); it('can redirect from componentless named outlets', fakeAsync(() => { - const router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmp); + const router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'main', outlet: 'aux', component: BlankCmp}, - {path: '', pathMatch: 'full', outlet: 'aux', redirectTo: 'main'}, - ]); + router.resetConfig([ + {path: 'main', outlet: 'aux', component: BlankCmp}, + {path: '', pathMatch: 'full', outlet: 'aux', redirectTo: 'main'}, + ]); - router.navigateByUrl(''); - advance(fixture); + router.navigateByUrl(''); + advance(fixture); - expect(TestBed.inject(Location).path()).toEqual('/(aux:main)'); - })); + expect(TestBed.inject(Location).path()).toEqual('/(aux:main)'); + })); - it('should update Navigation object after redirects are applied', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); - let initialUrl, afterRedirectUrl; + it('should update Navigation object after redirects are applied', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); + let initialUrl, afterRedirectUrl; - router.resetConfig([ - {path: 'old/team/:id', redirectTo: 'team/:id'}, {path: 'team/:id', component: TeamCmp} - ]); + router.resetConfig([ + {path: 'old/team/:id', redirectTo: 'team/:id'}, + {path: 'team/:id', component: TeamCmp}, + ]); - router.events.subscribe(e => { - if (e instanceof NavigationStart) { - const navigation = router.getCurrentNavigation(); - initialUrl = navigation && navigation.finalUrl; - } - if (e instanceof RoutesRecognized) { - const navigation = router.getCurrentNavigation(); - afterRedirectUrl = navigation && navigation.finalUrl; - } - }); + router.events.subscribe((e) => { + if (e instanceof NavigationStart) { + const navigation = router.getCurrentNavigation(); + initialUrl = navigation && navigation.finalUrl; + } + if (e instanceof RoutesRecognized) { + const navigation = router.getCurrentNavigation(); + afterRedirectUrl = navigation && navigation.finalUrl; + } + }); - router.navigateByUrl('old/team/22'); - advance(fixture); + router.navigateByUrl('old/team/22'); + advance(fixture); - expect(initialUrl).toBeUndefined(); - expect(router.serializeUrl(afterRedirectUrl as any)).toBe('/team/22'); - }))); + expect(initialUrl).toBeUndefined(); + expect(router.serializeUrl(afterRedirectUrl as any)).toBe('/team/22'); + }), + )); it('should not break the back button when trigger by location change', fakeAsync(() => { - TestBed.configureTestingModule( - {providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}]}); - const router = TestBed.inject(Router); - const location = TestBed.inject(Location); - const fixture = TestBed.createComponent(RootCmp); - advance(fixture); - router.resetConfig([ - {path: 'initial', component: BlankCmp}, {path: 'old/team/:id', redirectTo: 'team/:id'}, - {path: 'team/:id', component: TeamCmp} - ]); + TestBed.configureTestingModule({ + providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}], + }); + const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + const fixture = TestBed.createComponent(RootCmp); + advance(fixture); + router.resetConfig([ + {path: 'initial', component: BlankCmp}, + {path: 'old/team/:id', redirectTo: 'team/:id'}, + {path: 'team/:id', component: TeamCmp}, + ]); - location.go('initial'); - location.historyGo(0); - location.go('old/team/22'); - location.historyGo(0); + location.go('initial'); + location.historyGo(0); + location.go('old/team/22'); + location.historyGo(0); - // initial navigation - router.initialNavigation(); - advance(fixture); - expect(location.path()).toEqual('/team/22'); + // initial navigation + router.initialNavigation(); + advance(fixture); + expect(location.path()).toEqual('/team/22'); - location.back(); - advance(fixture); - expect(location.path()).toEqual('/initial'); + location.back(); + advance(fixture); + expect(location.path()).toEqual('/initial'); - // location change - location.go('/old/team/33'); - location.historyGo(0); + // location change + location.go('/old/team/33'); + location.historyGo(0); - advance(fixture); - expect(location.path()).toEqual('/team/33'); + advance(fixture); + expect(location.path()).toEqual('/team/33'); - location.back(); - advance(fixture); - expect(location.path()).toEqual('/initial'); - })); + location.back(); + advance(fixture); + expect(location.path()).toEqual('/initial'); + })); }); describe('guards', () => { describe('CanActivate', () => { describe('should not activate a route when CanActivate returns false', () => { beforeEach(() => { - TestBed.configureTestingModule( - {providers: [{provide: 'alwaysFalse', useValue: (a: any, b: any) => false}]}); + TestBed.configureTestingModule({ + providers: [{provide: 'alwaysFalse', useValue: (a: any, b: any) => false}], + }); }); - it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('works', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - const recordedEvents: Event[] = []; - router.events.forEach(e => recordedEvents.push(e)); + const recordedEvents: Event[] = []; + router.events.forEach((e) => recordedEvents.push(e)); - router.resetConfig( - [{path: 'team/:id', component: TeamCmp, canActivate: ['alwaysFalse']}]); + router.resetConfig([ + {path: 'team/:id', component: TeamCmp, canActivate: ['alwaysFalse']}, + ]); - router.navigateByUrl('/team/22'); - advance(fixture); + router.navigateByUrl('/team/22'); + advance(fixture); - expect(location.path()).toEqual(''); - expectEvents(recordedEvents, [ - [NavigationStart, '/team/22'], - [RoutesRecognized, '/team/22'], - [GuardsCheckStart, '/team/22'], - [ChildActivationStart], - [ActivationStart], - [GuardsCheckEnd, '/team/22'], - [NavigationCancel, '/team/22'], - ]); - expect((recordedEvents[5] as GuardsCheckEnd).shouldActivate).toBe(false); - }))); + expect(location.path()).toEqual(''); + expectEvents(recordedEvents, [ + [NavigationStart, '/team/22'], + [RoutesRecognized, '/team/22'], + [GuardsCheckStart, '/team/22'], + [ChildActivationStart], + [ActivationStart], + [GuardsCheckEnd, '/team/22'], + [NavigationCancel, '/team/22'], + ]); + expect((recordedEvents[5] as GuardsCheckEnd).shouldActivate).toBe(false); + }), + )); }); - describe( - 'should not activate a route when CanActivate returns false (componentless route)', - () => { - beforeEach(() => { - TestBed.configureTestingModule( - {providers: [{provide: 'alwaysFalse', useValue: (a: any, b: any) => false}]}); - }); - - it('works', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); - - router.resetConfig([{ - path: 'parent', - canActivate: ['alwaysFalse'], - children: [{path: 'team/:id', component: TeamCmp}] - }]); - - router.navigateByUrl('parent/team/22'); - advance(fixture); - - expect(location.path()).toEqual(''); - }))); + describe('should not activate a route when CanActivate returns false (componentless route)', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [{provide: 'alwaysFalse', useValue: (a: any, b: any) => false}], }); + }); + + it('works', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); + + router.resetConfig([ + { + path: 'parent', + canActivate: ['alwaysFalse'], + children: [{path: 'team/:id', component: TeamCmp}], + }, + ]); + + router.navigateByUrl('parent/team/22'); + advance(fixture); + + expect(location.path()).toEqual(''); + }), + )); + }); describe('should activate a route when CanActivate returns true', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [{ - provide: 'alwaysTrue', - useValue: (a: ActivatedRouteSnapshot, s: RouterStateSnapshot) => true - }] + providers: [ + { + provide: 'alwaysTrue', + useValue: (a: ActivatedRouteSnapshot, s: RouterStateSnapshot) => true, + }, + ], }); }); - it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('works', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig( - [{path: 'team/:id', component: TeamCmp, canActivate: ['alwaysTrue']}]); + router.resetConfig([ + {path: 'team/:id', component: TeamCmp, canActivate: ['alwaysTrue']}, + ]); - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); - }))); + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); + }), + )); }); describe('should work when given a class', () => { @@ -3361,77 +3644,88 @@ for (const browserAPI of ['navigation', 'history'] as const) { TestBed.configureTestingModule({providers: [AlwaysTrue]}); }); - it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('works', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig( - [{path: 'team/:id', component: TeamCmp, canActivate: [AlwaysTrue]}]); + router.resetConfig([ + {path: 'team/:id', component: TeamCmp, canActivate: [AlwaysTrue]}, + ]); - router.navigateByUrl('/team/22'); - advance(fixture); + router.navigateByUrl('/team/22'); + advance(fixture); - expect(location.path()).toEqual('/team/22'); - }))); + expect(location.path()).toEqual('/team/22'); + }), + )); }); describe('should work when returns an observable', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [{ - provide: 'CanActivate', - useValue: (a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { - return new Observable((observer) => { - observer.next(false); - }); - } - }] + providers: [ + { + provide: 'CanActivate', + useValue: (a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { + return new Observable((observer) => { + observer.next(false); + }); + }, + }, + ], }); }); + it('works', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + router.resetConfig([ + {path: 'team/:id', component: TeamCmp, canActivate: ['CanActivate']}, + ]); - router.resetConfig( - [{path: 'team/:id', component: TeamCmp, canActivate: ['CanActivate']}]); - - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual(''); - }))); + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual(''); + }), + )); }); describe('should work when returns a promise', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [{ - provide: 'CanActivate', - useValue: (a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { - if (a.params['id'] === '22') { - return Promise.resolve(true); - } else { - return Promise.resolve(false); - } - } - }] + providers: [ + { + provide: 'CanActivate', + useValue: (a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { + if (a.params['id'] === '22') { + return Promise.resolve(true); + } else { + return Promise.resolve(false); + } + }, + }, + ], }); }); + it('works', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + router.resetConfig([ + {path: 'team/:id', component: TeamCmp, canActivate: ['CanActivate']}, + ]); - router.resetConfig( - [{path: 'team/:id', component: TeamCmp, canActivate: ['CanActivate']}]); + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); - - router.navigateByUrl('/team/33'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); - }))); + router.navigateByUrl('/team/33'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); + }), + )); }); describe('should reset the location when cancelling a navigation', () => { @@ -3442,215 +3736,229 @@ for (const browserAPI of ['navigation', 'history'] as const) { provide: 'alwaysFalse', useValue: (a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { return false; - } + }, }, - {provide: LocationStrategy, useClass: HashLocationStrategy} - ] + {provide: LocationStrategy, useClass: HashLocationStrategy}, + ], }); }); - it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('works', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'one', component: SimpleCmp}, - {path: 'two', component: SimpleCmp, canActivate: ['alwaysFalse']} - ]); + router.resetConfig([ + {path: 'one', component: SimpleCmp}, + {path: 'two', component: SimpleCmp, canActivate: ['alwaysFalse']}, + ]); - router.navigateByUrl('/one'); - advance(fixture); - expect(location.path()).toEqual('/one'); + router.navigateByUrl('/one'); + advance(fixture); + expect(location.path()).toEqual('/one'); - location.go('/two'); - location.historyGo(0); - advance(fixture); - expect(location.path()).toEqual('/one'); - }))); + location.go('/two'); + location.historyGo(0); + advance(fixture); + expect(location.path()).toEqual('/one'); + }), + )); }); describe('should redirect to / when guard returns false', () => { - beforeEach(() => TestBed.configureTestingModule({ - providers: [{ - provide: 'returnFalseAndNavigate', - useFactory: (router: Router) => () => { - router.navigate(['/']); - return false; - }, - deps: [Router] - }] - })); + beforeEach(() => + TestBed.configureTestingModule({ + providers: [ + { + provide: 'returnFalseAndNavigate', + useFactory: (router: Router) => () => { + router.navigate(['/']); + return false; + }, + deps: [Router], + }, + ], + }), + ); - it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - router.resetConfig([ - { - path: '', - component: SimpleCmp, - }, - {path: 'one', component: RouteCmp, canActivate: ['returnFalseAndNavigate']} - ]); + it('works', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + router.resetConfig([ + { + path: '', + component: SimpleCmp, + }, + {path: 'one', component: RouteCmp, canActivate: ['returnFalseAndNavigate']}, + ]); - const fixture = TestBed.createComponent(RootCmp); - router.navigateByUrl('/one'); - advance(fixture); - expect(location.path()).toEqual(''); - expect(fixture.nativeElement).toHaveText('simple'); - }))); + const fixture = TestBed.createComponent(RootCmp); + router.navigateByUrl('/one'); + advance(fixture); + expect(location.path()).toEqual(''); + expect(fixture.nativeElement).toHaveText('simple'); + }), + )); }); describe('should redirect when guard returns UrlTree', () => { - beforeEach(() => TestBed.configureTestingModule({ - providers: [ - { - provide: 'returnUrlTree', - useFactory: (router: Router) => () => { - return router.parseUrl('/redirected'); + beforeEach(() => + TestBed.configureTestingModule({ + providers: [ + { + provide: 'returnUrlTree', + useFactory: (router: Router) => () => { + return router.parseUrl('/redirected'); + }, + deps: [Router], }, - deps: [Router] - }, - { - provide: 'returnRootUrlTree', - useFactory: (router: Router) => () => { - return router.parseUrl('/'); + { + provide: 'returnRootUrlTree', + useFactory: (router: Router) => () => { + return router.parseUrl('/'); + }, + deps: [Router], }, - deps: [Router] - } - ] + ], + }), + ); + + it('works', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const recordedEvents: Event[] = []; + let cancelEvent: NavigationCancel = null!; + router.events.forEach((e) => { + recordedEvents.push(e); + if (e instanceof NavigationCancel) cancelEvent = e; + }); + router.resetConfig([ + {path: '', component: SimpleCmp}, + {path: 'one', component: RouteCmp, canActivate: ['returnUrlTree']}, + {path: 'redirected', component: SimpleCmp}, + ]); + + const fixture = TestBed.createComponent(RootCmp); + router.navigateByUrl('/one'); + + advance(fixture); + + expect(location.path()).toEqual('/redirected'); + expect(fixture.nativeElement).toHaveText('simple'); + expect(cancelEvent && cancelEvent.reason).toBe( + 'NavigationCancelingError: Redirecting to "/redirected"', + ); + expectEvents(recordedEvents, [ + [NavigationStart, '/one'], + [RoutesRecognized, '/one'], + [GuardsCheckStart, '/one'], + [ChildActivationStart, undefined], + [ActivationStart, undefined], + [NavigationCancel, '/one'], + [NavigationStart, '/redirected'], + [RoutesRecognized, '/redirected'], + [GuardsCheckStart, '/redirected'], + [ChildActivationStart, undefined], + [ActivationStart, undefined], + [GuardsCheckEnd, '/redirected'], + [ResolveStart, '/redirected'], + [ResolveEnd, '/redirected'], + [ActivationEnd, undefined], + [ChildActivationEnd, undefined], + [NavigationEnd, '/redirected'], + ]); + }), + )); + + it('works with root url', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const recordedEvents: Event[] = []; + let cancelEvent: NavigationCancel = null!; + router.events.forEach((e: any) => { + recordedEvents.push(e); + if (e instanceof NavigationCancel) cancelEvent = e; + }); + router.resetConfig([ + {path: '', component: SimpleCmp}, + {path: 'one', component: RouteCmp, canActivate: ['returnRootUrlTree']}, + ]); + + const fixture = TestBed.createComponent(RootCmp); + router.navigateByUrl('/one'); + + advance(fixture); + + expect(location.path()).toEqual(''); + expect(fixture.nativeElement).toHaveText('simple'); + expect(cancelEvent && cancelEvent.reason).toBe( + 'NavigationCancelingError: Redirecting to "/"', + ); + expectEvents(recordedEvents, [ + [NavigationStart, '/one'], + [RoutesRecognized, '/one'], + [GuardsCheckStart, '/one'], + [ChildActivationStart, undefined], + [ActivationStart, undefined], + [NavigationCancel, '/one'], + [NavigationStart, '/'], + [RoutesRecognized, '/'], + [GuardsCheckStart, '/'], + [ChildActivationStart, undefined], + [ActivationStart, undefined], + [GuardsCheckEnd, '/'], + [ResolveStart, '/'], + [ResolveEnd, '/'], + [ActivationEnd, undefined], + [ChildActivationEnd, undefined], + [NavigationEnd, '/'], + ]); + }), + )); + + it('replaces URL when URL is updated eagerly so back button can still work', fakeAsync(() => { + TestBed.configureTestingModule({ + providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))], + }); + const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + router.resetConfig([ + {path: '', component: SimpleCmp}, + {path: 'one', component: RouteCmp, canActivate: ['returnUrlTree']}, + {path: 'redirected', component: SimpleCmp}, + ]); + createRoot(router, RootCmp); + router.navigateByUrl('/one'); + const urlChanges: string[] = []; + location.onUrlChange((change) => { + urlChanges.push(change); + }); + + tick(); + + expect(location.path()).toEqual('/redirected'); + expect(urlChanges).toEqual(['/one', '/redirected']); + location.back(); + tick(); + expect(location.path()).toEqual(''); })); - it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const recordedEvents: Event[] = []; - let cancelEvent: NavigationCancel = null!; - router.events.forEach((e) => { - recordedEvents.push(e); - if (e instanceof NavigationCancel) cancelEvent = e; - }); - router.resetConfig([ - {path: '', component: SimpleCmp}, - {path: 'one', component: RouteCmp, canActivate: ['returnUrlTree']}, - {path: 'redirected', component: SimpleCmp} - ]); - - const fixture = TestBed.createComponent(RootCmp); - router.navigateByUrl('/one'); - - advance(fixture); - - expect(location.path()).toEqual('/redirected'); - expect(fixture.nativeElement).toHaveText('simple'); - expect(cancelEvent && cancelEvent.reason) - .toBe('NavigationCancelingError: Redirecting to "/redirected"'); - expectEvents(recordedEvents, [ - [NavigationStart, '/one'], - [RoutesRecognized, '/one'], - [GuardsCheckStart, '/one'], - [ChildActivationStart, undefined], - [ActivationStart, undefined], - [NavigationCancel, '/one'], - [NavigationStart, '/redirected'], - [RoutesRecognized, '/redirected'], - [GuardsCheckStart, '/redirected'], - [ChildActivationStart, undefined], - [ActivationStart, undefined], - [GuardsCheckEnd, '/redirected'], - [ResolveStart, '/redirected'], - [ResolveEnd, '/redirected'], - [ActivationEnd, undefined], - [ChildActivationEnd, undefined], - [NavigationEnd, '/redirected'], - ]); - }))); - - it('works with root url', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const recordedEvents: Event[] = []; - let cancelEvent: NavigationCancel = null!; - router.events.forEach((e: any) => { - recordedEvents.push(e); - if (e instanceof NavigationCancel) cancelEvent = e; - }); - router.resetConfig([ - {path: '', component: SimpleCmp}, - {path: 'one', component: RouteCmp, canActivate: ['returnRootUrlTree']} - ]); - - const fixture = TestBed.createComponent(RootCmp); - router.navigateByUrl('/one'); - - advance(fixture); - - expect(location.path()).toEqual(''); - expect(fixture.nativeElement).toHaveText('simple'); - expect(cancelEvent && cancelEvent.reason) - .toBe('NavigationCancelingError: Redirecting to "/"'); - expectEvents(recordedEvents, [ - [NavigationStart, '/one'], - [RoutesRecognized, '/one'], - [GuardsCheckStart, '/one'], - [ChildActivationStart, undefined], - [ActivationStart, undefined], - [NavigationCancel, '/one'], - [NavigationStart, '/'], - [RoutesRecognized, '/'], - [GuardsCheckStart, '/'], - [ChildActivationStart, undefined], - [ActivationStart, undefined], - [GuardsCheckEnd, '/'], - [ResolveStart, '/'], - [ResolveEnd, '/'], - [ActivationEnd, undefined], - [ChildActivationEnd, undefined], - [NavigationEnd, '/'], - ]); - }))); - - it('replaces URL when URL is updated eagerly so back button can still work', - fakeAsync(() => { - TestBed.configureTestingModule({ - providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))] - }); - const router = TestBed.inject(Router); - const location = TestBed.inject(Location); - router.resetConfig([ - {path: '', component: SimpleCmp}, - {path: 'one', component: RouteCmp, canActivate: ['returnUrlTree']}, - {path: 'redirected', component: SimpleCmp} - ]); - createRoot(router, RootCmp); - router.navigateByUrl('/one'); - const urlChanges: string[] = []; - location.onUrlChange((change) => { - urlChanges.push(change); - }); - - tick(); - - expect(location.path()).toEqual('/redirected'); - expect(urlChanges).toEqual(['/one', '/redirected']); - location.back(); - tick(); - expect(location.path()).toEqual(''); - })); - it('should resolve navigateByUrl promise after redirect finishes', fakeAsync(() => { - TestBed.configureTestingModule({ - providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))] - }); - const router = TestBed.inject(Router); - const location = TestBed.inject(Location); - let resolvedPath = ''; - router.resetConfig([ - {path: '', component: SimpleCmp}, - {path: 'one', component: RouteCmp, canActivate: ['returnUrlTree']}, - {path: 'redirected', component: SimpleCmp} - ]); - const fixture = createRoot(router, RootCmp); - router.navigateByUrl('/one').then(v => { - resolvedPath = location.path(); - }); + TestBed.configureTestingModule({ + providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))], + }); + const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + let resolvedPath = ''; + router.resetConfig([ + {path: '', component: SimpleCmp}, + {path: 'one', component: RouteCmp, canActivate: ['returnUrlTree']}, + {path: 'redirected', component: SimpleCmp}, + ]); + const fixture = createRoot(router, RootCmp); + router.navigateByUrl('/one').then((v) => { + resolvedPath = location.path(); + }); - tick(); - expect(resolvedPath).toBe('/redirected'); - })); + tick(); + expect(resolvedPath).toBe('/redirected'); + })); }); describe('runGuardsAndResolvers', () => { @@ -3667,15 +3975,17 @@ for (const browserAPI of ['navigation', 'history'] as const) { useValue: () => { guardRunCount++; return true; - } + }, }, - {provide: 'resolver', useValue: () => resolverRunCount++} - ] + {provide: 'resolver', useValue: () => resolverRunCount++}, + ], }); }); - function configureRouter(router: Router, runGuardsAndResolvers: RunGuardsAndResolvers): - ComponentFixture { + function configureRouter( + router: Router, + runGuardsAndResolvers: RunGuardsAndResolvers, + ): ComponentFixture { const fixture = createRoot(router, RootCmpWithTwoOutlets); router.resetConfig([ @@ -3684,14 +3994,15 @@ for (const browserAPI of ['navigation', 'history'] as const) { runGuardsAndResolvers, component: RouteCmp, canActivate: ['guard'], - resolve: {data: 'resolver'} + resolve: {data: 'resolver'}, }, - {path: 'b', component: SimpleCmp, outlet: 'right'}, { + {path: 'b', component: SimpleCmp, outlet: 'right'}, + { path: 'c/:param', runGuardsAndResolvers, component: RouteCmp, canActivate: ['guard'], - resolve: {data: 'resolver'} + resolve: {data: 'resolver'}, }, { path: 'd/:param', @@ -3704,15 +4015,15 @@ for (const browserAPI of ['navigation', 'history'] as const) { canActivate: ['guard'], resolve: {data: 'resolver'}, }, - ] + ], }, { path: 'throwing', runGuardsAndResolvers, component: ThrowingCmp, canActivate: ['guard'], - resolve: {data: 'resolver'} - } + resolve: {data: 'resolver'}, + }, ]); router.navigateByUrl('/a'); @@ -3720,256 +4031,260 @@ for (const browserAPI of ['navigation', 'history'] as const) { return fixture; } + it('should rerun guards and resolvers when params change', fakeAsync( + inject([Router], (router: Router) => { + const fixture = configureRouter(router, 'paramsChange'); - it('should rerun guards and resolvers when params change', - fakeAsync(inject([Router], (router: Router) => { - const fixture = configureRouter(router, 'paramsChange'); + const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance; + const recordedData: Data[] = []; + cmp.route.data.subscribe((data) => recordedData.push(data)); - const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance; - const recordedData: Data[] = []; - cmp.route.data.subscribe((data) => recordedData.push(data)); + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); - expect(guardRunCount).toEqual(1); - expect(recordedData).toEqual([{data: 0}]); + router.navigateByUrl('/a;p=1'); + advance(fixture); + expect(guardRunCount).toEqual(2); + expect(recordedData).toEqual([{data: 0}, {data: 1}]); - router.navigateByUrl('/a;p=1'); - advance(fixture); - expect(guardRunCount).toEqual(2); - expect(recordedData).toEqual([{data: 0}, {data: 1}]); + router.navigateByUrl('/a;p=2'); + advance(fixture); + expect(guardRunCount).toEqual(3); + expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}]); - router.navigateByUrl('/a;p=2'); - advance(fixture); - expect(guardRunCount).toEqual(3); - expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}]); + router.navigateByUrl('/a;p=2?q=1'); + advance(fixture); + expect(guardRunCount).toEqual(3); + expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}]); + }), + )); - router.navigateByUrl('/a;p=2?q=1'); - advance(fixture); - expect(guardRunCount).toEqual(3); - expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}]); - }))); + it('should rerun guards and resolvers when query params change', fakeAsync( + inject([Router], (router: Router) => { + const fixture = configureRouter(router, 'paramsOrQueryParamsChange'); - it('should rerun guards and resolvers when query params change', - fakeAsync(inject([Router], (router: Router) => { - const fixture = configureRouter(router, 'paramsOrQueryParamsChange'); + const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance; + const recordedData: Data[] = []; + cmp.route.data.subscribe((data) => recordedData.push(data)); - const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance; - const recordedData: Data[] = []; - cmp.route.data.subscribe((data) => recordedData.push(data)); + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); - expect(guardRunCount).toEqual(1); - expect(recordedData).toEqual([{data: 0}]); + router.navigateByUrl('/a;p=1'); + advance(fixture); + expect(guardRunCount).toEqual(2); + expect(recordedData).toEqual([{data: 0}, {data: 1}]); - router.navigateByUrl('/a;p=1'); - advance(fixture); - expect(guardRunCount).toEqual(2); - expect(recordedData).toEqual([{data: 0}, {data: 1}]); + router.navigateByUrl('/a;p=2'); + advance(fixture); + expect(guardRunCount).toEqual(3); + expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}]); - router.navigateByUrl('/a;p=2'); - advance(fixture); - expect(guardRunCount).toEqual(3); - expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}]); + router.navigateByUrl('/a;p=2?q=1'); + advance(fixture); + expect(guardRunCount).toEqual(4); + expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}, {data: 3}]); - router.navigateByUrl('/a;p=2?q=1'); - advance(fixture); - expect(guardRunCount).toEqual(4); - expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}, {data: 3}]); + router.navigateByUrl('/a;p=2(right:b)?q=1'); + advance(fixture); + expect(guardRunCount).toEqual(4); + expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}, {data: 3}]); + }), + )); - router.navigateByUrl('/a;p=2(right:b)?q=1'); - advance(fixture); - expect(guardRunCount).toEqual(4); - expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}, {data: 3}]); - }))); + it('should always rerun guards and resolvers', fakeAsync( + inject([Router], (router: Router) => { + const fixture = configureRouter(router, 'always'); - it('should always rerun guards and resolvers', - fakeAsync(inject([Router], (router: Router) => { - const fixture = configureRouter(router, 'always'); + const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance; + const recordedData: Data[] = []; + cmp.route.data.subscribe((data) => recordedData.push(data)); - const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance; - const recordedData: Data[] = []; - cmp.route.data.subscribe((data) => recordedData.push(data)); + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); - expect(guardRunCount).toEqual(1); - expect(recordedData).toEqual([{data: 0}]); + router.navigateByUrl('/a;p=1'); + advance(fixture); + expect(guardRunCount).toEqual(2); + expect(recordedData).toEqual([{data: 0}, {data: 1}]); - router.navigateByUrl('/a;p=1'); - advance(fixture); - expect(guardRunCount).toEqual(2); - expect(recordedData).toEqual([{data: 0}, {data: 1}]); + router.navigateByUrl('/a;p=2'); + advance(fixture); + expect(guardRunCount).toEqual(3); + expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}]); - router.navigateByUrl('/a;p=2'); - advance(fixture); - expect(guardRunCount).toEqual(3); - expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}]); + router.navigateByUrl('/a;p=2?q=1'); + advance(fixture); + expect(guardRunCount).toEqual(4); + expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}, {data: 3}]); - router.navigateByUrl('/a;p=2?q=1'); - advance(fixture); - expect(guardRunCount).toEqual(4); - expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}, {data: 3}]); + router.navigateByUrl('/a;p=2(right:b)?q=1'); + advance(fixture); + expect(guardRunCount).toEqual(5); + expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}, {data: 3}, {data: 4}]); - router.navigateByUrl('/a;p=2(right:b)?q=1'); - advance(fixture); - expect(guardRunCount).toEqual(5); - expect(recordedData).toEqual([ - {data: 0}, {data: 1}, {data: 2}, {data: 3}, {data: 4} - ]); + // Issue #39030, always running guards and resolvers should not throw + // when navigating away from a component with a throwing constructor. + expect(() => { + router.navigateByUrl('/throwing').catch(() => {}); + advance(fixture); + router.navigateByUrl('/a;p=1'); + advance(fixture); + }).not.toThrow(); + }), + )); - // Issue #39030, always running guards and resolvers should not throw - // when navigating away from a component with a throwing constructor. - expect(() => { - router.navigateByUrl('/throwing').catch(() => {}); - advance(fixture); - router.navigateByUrl('/a;p=1'); - advance(fixture); - }).not.toThrow(); - }))); + it('should rerun rerun guards and resolvers when path params change', fakeAsync( + inject([Router], (router: Router) => { + const fixture = configureRouter(router, 'pathParamsChange'); - it('should rerun rerun guards and resolvers when path params change', - fakeAsync(inject([Router], (router: Router) => { - const fixture = configureRouter(router, 'pathParamsChange'); + const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance; + const recordedData: Data[] = []; + cmp.route.data.subscribe((data) => recordedData.push(data)); - const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance; - const recordedData: Data[] = []; - cmp.route.data.subscribe((data) => recordedData.push(data)); + // First navigation has already run + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); - // First navigation has already run - expect(guardRunCount).toEqual(1); - expect(recordedData).toEqual([{data: 0}]); + // Changing any optional params will not result in running guards or resolvers + router.navigateByUrl('/a;p=1'); + advance(fixture); + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); - // Changing any optional params will not result in running guards or resolvers - router.navigateByUrl('/a;p=1'); - advance(fixture); - expect(guardRunCount).toEqual(1); - expect(recordedData).toEqual([{data: 0}]); + router.navigateByUrl('/a;p=2'); + advance(fixture); + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); - router.navigateByUrl('/a;p=2'); - advance(fixture); - expect(guardRunCount).toEqual(1); - expect(recordedData).toEqual([{data: 0}]); + router.navigateByUrl('/a;p=2?q=1'); + advance(fixture); + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); - router.navigateByUrl('/a;p=2?q=1'); - advance(fixture); - expect(guardRunCount).toEqual(1); - expect(recordedData).toEqual([{data: 0}]); + router.navigateByUrl('/a;p=2(right:b)?q=1'); + advance(fixture); + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); - router.navigateByUrl('/a;p=2(right:b)?q=1'); - advance(fixture); - expect(guardRunCount).toEqual(1); - expect(recordedData).toEqual([{data: 0}]); + // Change to new route with path param should run guards and resolvers + router.navigateByUrl('/c/paramValue'); + advance(fixture); - // Change to new route with path param should run guards and resolvers - router.navigateByUrl('/c/paramValue'); - advance(fixture); + expect(guardRunCount).toEqual(2); - expect(guardRunCount).toEqual(2); + // Modifying a path param should run guards and resolvers + router.navigateByUrl('/c/paramValueChanged'); + advance(fixture); + expect(guardRunCount).toEqual(3); - // Modifying a path param should run guards and resolvers - router.navigateByUrl('/c/paramValueChanged'); - advance(fixture); - expect(guardRunCount).toEqual(3); + // Adding optional params should not cause guards/resolvers to run + router.navigateByUrl('/c/paramValueChanged;p=1?q=2'); + advance(fixture); + expect(guardRunCount).toEqual(3); + }), + )); - // Adding optional params should not cause guards/resolvers to run - router.navigateByUrl('/c/paramValueChanged;p=1?q=2'); - advance(fixture); - expect(guardRunCount).toEqual(3); - }))); + it('should rerun when a parent segment changes', fakeAsync( + inject([Router], (router: Router) => { + const fixture = configureRouter(router, 'pathParamsChange'); - it('should rerun when a parent segment changes', - fakeAsync(inject([Router], (router: Router) => { - const fixture = configureRouter(router, 'pathParamsChange'); + const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance; - const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance; + // Land on an initial page + router.navigateByUrl('/d/1;dd=11/e/2;dd=22'); + advance(fixture); - // Land on an initial page - router.navigateByUrl('/d/1;dd=11/e/2;dd=22'); - advance(fixture); + expect(guardRunCount).toEqual(2); - expect(guardRunCount).toEqual(2); + // Changes cause re-run on the config with the guard + router.navigateByUrl('/d/1;dd=11/e/3;ee=22'); + advance(fixture); - // Changes cause re-run on the config with the guard - router.navigateByUrl('/d/1;dd=11/e/3;ee=22'); - advance(fixture); + expect(guardRunCount).toEqual(3); - expect(guardRunCount).toEqual(3); + // Changes to the parent also cause re-run + router.navigateByUrl('/d/2;dd=11/e/3;ee=22'); + advance(fixture); - // Changes to the parent also cause re-run - router.navigateByUrl('/d/2;dd=11/e/3;ee=22'); - advance(fixture); + expect(guardRunCount).toEqual(4); + }), + )); - expect(guardRunCount).toEqual(4); - }))); + it('should rerun rerun guards and resolvers when path or query params change', fakeAsync( + inject([Router], (router: Router) => { + const fixture = configureRouter(router, 'pathParamsOrQueryParamsChange'); - it('should rerun rerun guards and resolvers when path or query params change', - fakeAsync(inject([Router], (router: Router) => { - const fixture = configureRouter(router, 'pathParamsOrQueryParamsChange'); + const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance; + const recordedData: Data[] = []; + cmp.route.data.subscribe((data) => recordedData.push(data)); - const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance; - const recordedData: Data[] = []; - cmp.route.data.subscribe((data) => recordedData.push(data)); + // First navigation has already run + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); - // First navigation has already run - expect(guardRunCount).toEqual(1); - expect(recordedData).toEqual([{data: 0}]); + // Changing matrix params will not result in running guards or resolvers + router.navigateByUrl('/a;p=1'); + advance(fixture); + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); - // Changing matrix params will not result in running guards or resolvers - router.navigateByUrl('/a;p=1'); - advance(fixture); - expect(guardRunCount).toEqual(1); - expect(recordedData).toEqual([{data: 0}]); + router.navigateByUrl('/a;p=2'); + advance(fixture); + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); - router.navigateByUrl('/a;p=2'); - advance(fixture); - expect(guardRunCount).toEqual(1); - expect(recordedData).toEqual([{data: 0}]); + // Adding query params will re-run guards/resolvers + router.navigateByUrl('/a;p=2?q=1'); + advance(fixture); + expect(guardRunCount).toEqual(2); + expect(recordedData).toEqual([{data: 0}, {data: 1}]); - // Adding query params will re-run guards/resolvers - router.navigateByUrl('/a;p=2?q=1'); - advance(fixture); - expect(guardRunCount).toEqual(2); - expect(recordedData).toEqual([{data: 0}, {data: 1}]); + // Changing query params will re-run guards/resolvers + router.navigateByUrl('/a;p=2?q=2'); + advance(fixture); + expect(guardRunCount).toEqual(3); + expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}]); + }), + )); - // Changing query params will re-run guards/resolvers - router.navigateByUrl('/a;p=2?q=2'); - advance(fixture); - expect(guardRunCount).toEqual(3); - expect(recordedData).toEqual([{data: 0}, {data: 1}, {data: 2}]); - }))); + it('should allow a predicate function to determine when to run guards and resolvers', fakeAsync( + inject([Router], (router: Router) => { + const fixture = configureRouter(router, (from, to) => to.paramMap.get('p') === '2'); - it('should allow a predicate function to determine when to run guards and resolvers', - fakeAsync(inject([Router], (router: Router) => { - const fixture = configureRouter(router, (from, to) => to.paramMap.get('p') === '2'); + const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance; + const recordedData: Data[] = []; + cmp.route.data.subscribe((data) => recordedData.push(data)); - const cmp: RouteCmp = fixture.debugElement.children[1].componentInstance; - const recordedData: Data[] = []; - cmp.route.data.subscribe((data) => recordedData.push(data)); + // First navigation has already run + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); - // First navigation has already run - expect(guardRunCount).toEqual(1); - expect(recordedData).toEqual([{data: 0}]); + // Adding `p` param shouldn't cause re-run + router.navigateByUrl('/a;p=1'); + advance(fixture); + expect(guardRunCount).toEqual(1); + expect(recordedData).toEqual([{data: 0}]); - // Adding `p` param shouldn't cause re-run - router.navigateByUrl('/a;p=1'); - advance(fixture); - expect(guardRunCount).toEqual(1); - expect(recordedData).toEqual([{data: 0}]); + // Re-run should trigger on p=2 + router.navigateByUrl('/a;p=2'); + advance(fixture); + expect(guardRunCount).toEqual(2); + expect(recordedData).toEqual([{data: 0}, {data: 1}]); - // Re-run should trigger on p=2 - router.navigateByUrl('/a;p=2'); - advance(fixture); - expect(guardRunCount).toEqual(2); - expect(recordedData).toEqual([{data: 0}, {data: 1}]); + // Any other changes don't pass the predicate + router.navigateByUrl('/a;p=3?q=1'); + advance(fixture); + expect(guardRunCount).toEqual(2); + expect(recordedData).toEqual([{data: 0}, {data: 1}]); - // Any other changes don't pass the predicate - router.navigateByUrl('/a;p=3?q=1'); - advance(fixture); - expect(guardRunCount).toEqual(2); - expect(recordedData).toEqual([{data: 0}, {data: 1}]); - - // Changing query params will re-run guards/resolvers - router.navigateByUrl('/a;p=3?q=2'); - advance(fixture); - expect(guardRunCount).toEqual(2); - expect(recordedData).toEqual([{data: 0}, {data: 1}]); - }))); + // Changing query params will re-run guards/resolvers + router.navigateByUrl('/a;p=3?q=2'); + advance(fixture); + expect(guardRunCount).toEqual(2); + expect(recordedData).toEqual([{data: 0}, {data: 1}]); + }), + )); }); describe('should wait for parent to complete', () => { @@ -3986,7 +4301,7 @@ for (const browserAPI of ['navigation', 'history'] as const) { log.push('parent'); return true; }); - } + }, }, { provide: 'childGuard', @@ -3995,35 +4310,37 @@ for (const browserAPI of ['navigation', 'history'] as const) { log.push('child'); return true; }); - } - } - ] + }, + }, + ], }); }); function delayPromise(delay: number): Promise { let resolve: (val: boolean) => void; - const promise = new Promise(res => resolve = res); + const promise = new Promise((res) => (resolve = res)); setTimeout(() => resolve(true), delay); return promise; } - it('works', fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('works', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'parent', - canActivate: ['parentGuard'], - children: [ - {path: 'child', component: SimpleCmp, canActivate: ['childGuard']}, - ] - }]); + router.resetConfig([ + { + path: 'parent', + canActivate: ['parentGuard'], + children: [{path: 'child', component: SimpleCmp, canActivate: ['childGuard']}], + }, + ]); - router.navigateByUrl('/parent/child'); - advance(fixture); - tick(15); - expect(log).toEqual(['parent', 'child']); - }))); + router.navigateByUrl('/parent/child'); + advance(fixture); + tick(15); + expect(log).toEqual(['parent', 'child']); + }), + )); }); }); @@ -4039,311 +4356,341 @@ for (const browserAPI of ['navigation', 'history'] as const) { provide: 'CanDeactivateParent', useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { return a.params['id'] === '22'; - } + }, }, { provide: 'CanDeactivateTeam', useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { return c.route.snapshot.params['id'] === '22'; - } + }, }, { provide: 'CanDeactivateUser', useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { return a.params['name'] === 'victor'; - } + }, }, { provide: 'RecordingDeactivate', useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { log.push({path: a.routeConfig!.path, component: c}); return true; - } + }, }, { provide: 'alwaysFalse', useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { return false; - } + }, }, { provide: 'alwaysFalseAndLogging', useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { log.push('called'); return false; - } + }, }, { provide: 'alwaysFalseWithDelayAndLogging', useValue: () => { log.push('called'); let resolve: (result: boolean) => void; - const promise = new Promise(res => resolve = res); + const promise = new Promise((res) => (resolve = res)); setTimeout(() => resolve(false), 0); return promise; - } + }, }, { provide: 'canActivate_alwaysTrueAndLogging', useValue: () => { log.push('canActivate called'); return true; - } + }, }, - ] + ], }); }); describe('should not deactivate a route when CanDeactivate returns false', () => { - it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('works', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig( - [{path: 'team/:id', component: TeamCmp, canDeactivate: ['CanDeactivateTeam']}]); + router.resetConfig([ + {path: 'team/:id', component: TeamCmp, canDeactivate: ['CanDeactivateTeam']}, + ]); - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); - let successStatus: boolean = false; - router.navigateByUrl('/team/33')!.then(res => successStatus = res); - advance(fixture); - expect(location.path()).toEqual('/team/33'); - expect(successStatus).toEqual(true); + let successStatus: boolean = false; + router.navigateByUrl('/team/33')!.then((res) => (successStatus = res)); + advance(fixture); + expect(location.path()).toEqual('/team/33'); + expect(successStatus).toEqual(true); - let canceledStatus: boolean = false; - router.navigateByUrl('/team/44')!.then(res => canceledStatus = res); - advance(fixture); - expect(location.path()).toEqual('/team/33'); - expect(canceledStatus).toEqual(false); - }))); + let canceledStatus: boolean = false; + router.navigateByUrl('/team/44')!.then((res) => (canceledStatus = res)); + advance(fixture); + expect(location.path()).toEqual('/team/33'); + expect(canceledStatus).toEqual(false); + }), + )); - it('works with componentless routes', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('works with componentless routes', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - { - path: 'grandparent', - canDeactivate: ['RecordingDeactivate'], - children: [{ - path: 'parent', - canDeactivate: ['RecordingDeactivate'], - children: [{ - path: 'child', - canDeactivate: ['RecordingDeactivate'], - children: [{ - path: 'simple', - component: SimpleCmp, - canDeactivate: ['RecordingDeactivate'] - }] - }] - }] - }, - {path: 'simple', component: SimpleCmp} - ]); + router.resetConfig([ + { + path: 'grandparent', + canDeactivate: ['RecordingDeactivate'], + children: [ + { + path: 'parent', + canDeactivate: ['RecordingDeactivate'], + children: [ + { + path: 'child', + canDeactivate: ['RecordingDeactivate'], + children: [ + { + path: 'simple', + component: SimpleCmp, + canDeactivate: ['RecordingDeactivate'], + }, + ], + }, + ], + }, + ], + }, + {path: 'simple', component: SimpleCmp}, + ]); - router.navigateByUrl('/grandparent/parent/child/simple'); - advance(fixture); - expect(location.path()).toEqual('/grandparent/parent/child/simple'); + router.navigateByUrl('/grandparent/parent/child/simple'); + advance(fixture); + expect(location.path()).toEqual('/grandparent/parent/child/simple'); - router.navigateByUrl('/simple'); - advance(fixture); + router.navigateByUrl('/simple'); + advance(fixture); - const child = fixture.debugElement.children[1].componentInstance; + const child = fixture.debugElement.children[1].componentInstance; - expect(log.map((a: any) => a.path)).toEqual([ - 'simple', 'child', 'parent', 'grandparent' - ]); - expect(log[0].component instanceof SimpleCmp).toBeTruthy(); - [1, 2, 3].forEach(i => expect(log[i].component).toBeNull()); - expect(child instanceof SimpleCmp).toBeTruthy(); - expect(child).not.toBe(log[0].component); - }))); + expect(log.map((a: any) => a.path)).toEqual([ + 'simple', + 'child', + 'parent', + 'grandparent', + ]); + expect(log[0].component instanceof SimpleCmp).toBeTruthy(); + [1, 2, 3].forEach((i) => expect(log[i].component).toBeNull()); + expect(child instanceof SimpleCmp).toBeTruthy(); + expect(child).not.toBe(log[0].component); + }), + )); - it('works with aux routes', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('works with aux routes', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'two-outlets', - component: TwoOutletsCmp, - children: [ - {path: 'a', component: BlankCmp}, { - path: 'b', - canDeactivate: ['RecordingDeactivate'], - component: SimpleCmp, - outlet: 'aux' - } - ] - }]); + router.resetConfig([ + { + path: 'two-outlets', + component: TwoOutletsCmp, + children: [ + {path: 'a', component: BlankCmp}, + { + path: 'b', + canDeactivate: ['RecordingDeactivate'], + component: SimpleCmp, + outlet: 'aux', + }, + ], + }, + ]); - router.navigateByUrl('/two-outlets/(a//aux:b)'); - advance(fixture); - expect(location.path()).toEqual('/two-outlets/(a//aux:b)'); + router.navigateByUrl('/two-outlets/(a//aux:b)'); + advance(fixture); + expect(location.path()).toEqual('/two-outlets/(a//aux:b)'); - router.navigate(['two-outlets', {outlets: {aux: null}}]); - advance(fixture); + router.navigate(['two-outlets', {outlets: {aux: null}}]); + advance(fixture); - expect(log.map((a: any) => a.path)).toEqual(['b']); - expect(location.path()).toEqual('/two-outlets/a'); - }))); + expect(log.map((a: any) => a.path)).toEqual(['b']); + expect(location.path()).toEqual('/two-outlets/a'); + }), + )); - it('works with a nested route', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('works with a nested route', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: '', pathMatch: 'full', component: SimpleCmp}, - {path: 'user/:name', component: UserCmp, canDeactivate: ['CanDeactivateUser']} - ] - }]); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + {path: '', pathMatch: 'full', component: SimpleCmp}, + {path: 'user/:name', component: UserCmp, canDeactivate: ['CanDeactivateUser']}, + ], + }, + ]); - router.navigateByUrl('/team/22/user/victor'); - advance(fixture); + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); - // this works because we can deactivate victor - router.navigateByUrl('/team/33'); - advance(fixture); - expect(location.path()).toEqual('/team/33'); + // this works because we can deactivate victor + router.navigateByUrl('/team/33'); + advance(fixture); + expect(location.path()).toEqual('/team/33'); - router.navigateByUrl('/team/33/user/fedor'); - advance(fixture); + router.navigateByUrl('/team/33/user/fedor'); + advance(fixture); - // this doesn't work cause we cannot deactivate fedor - router.navigateByUrl('/team/44'); - advance(fixture); - expect(location.path()).toEqual('/team/33/user/fedor'); - }))); + // this doesn't work cause we cannot deactivate fedor + router.navigateByUrl('/team/44'); + advance(fixture); + expect(location.path()).toEqual('/team/33/user/fedor'); + }), + )); }); - it('should use correct component to deactivate forChild route', - fakeAsync(inject([Router], (router: Router) => { - @Component({selector: 'admin', template: ''}) - class AdminComponent { - } + it('should use correct component to deactivate forChild route', fakeAsync( + inject([Router], (router: Router) => { + @Component({selector: 'admin', template: ''}) + class AdminComponent {} - @NgModule({ - declarations: [AdminComponent], - imports: [RouterModule.forChild([{ - path: '', - component: AdminComponent, - canDeactivate: ['RecordingDeactivate'], - }])], - }) - class LazyLoadedModule { - } + @NgModule({ + declarations: [AdminComponent], + imports: [ + RouterModule.forChild([ + { + path: '', + component: AdminComponent, + canDeactivate: ['RecordingDeactivate'], + }, + ]), + ], + }) + class LazyLoadedModule {} - const fixture = createRoot(router, RootCmp); + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - { - path: 'a', - component: WrapperCmp, - children: [ - {path: '', pathMatch: 'full', loadChildren: () => LazyLoadedModule}, - ] - }, - {path: 'b', component: SimpleCmp}, - ]); + router.resetConfig([ + { + path: 'a', + component: WrapperCmp, + children: [{path: '', pathMatch: 'full', loadChildren: () => LazyLoadedModule}], + }, + {path: 'b', component: SimpleCmp}, + ]); - router.navigateByUrl('/a'); - advance(fixture); - router.navigateByUrl('/b'); - advance(fixture); + router.navigateByUrl('/a'); + advance(fixture); + router.navigateByUrl('/b'); + advance(fixture); - expect(log[0].component).toBeInstanceOf(AdminComponent); - }))); + expect(log[0].component).toBeInstanceOf(AdminComponent); + }), + )); - it('should not create a route state if navigation is canceled', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should not create a route state if navigation is canceled', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'main', - component: TeamCmp, - children: [ - {path: 'component1', component: SimpleCmp, canDeactivate: ['alwaysFalse']}, - {path: 'component2', component: SimpleCmp} - ] - }]); + router.resetConfig([ + { + path: 'main', + component: TeamCmp, + children: [ + {path: 'component1', component: SimpleCmp, canDeactivate: ['alwaysFalse']}, + {path: 'component2', component: SimpleCmp}, + ], + }, + ]); - router.navigateByUrl('/main/component1'); - advance(fixture); + router.navigateByUrl('/main/component1'); + advance(fixture); - router.navigateByUrl('/main/component2'); - advance(fixture); + router.navigateByUrl('/main/component2'); + advance(fixture); - const teamCmp = fixture.debugElement.children[1].componentInstance; - expect(teamCmp.route.firstChild.url.value[0].path).toEqual('component1'); - expect(location.path()).toEqual('/main/component1'); - }))); + const teamCmp = fixture.debugElement.children[1].componentInstance; + expect(teamCmp.route.firstChild.url.value[0].path).toEqual('component1'); + expect(location.path()).toEqual('/main/component1'); + }), + )); - it('should not run CanActivate when CanDeactivate returns false', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should not run CanActivate when CanDeactivate returns false', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'main', - component: TeamCmp, - children: [ - { - path: 'component1', - component: SimpleCmp, - canDeactivate: ['alwaysFalseWithDelayAndLogging'] - }, - { - path: 'component2', - component: SimpleCmp, - canActivate: ['canActivate_alwaysTrueAndLogging'] - }, - ] - }]); + router.resetConfig([ + { + path: 'main', + component: TeamCmp, + children: [ + { + path: 'component1', + component: SimpleCmp, + canDeactivate: ['alwaysFalseWithDelayAndLogging'], + }, + { + path: 'component2', + component: SimpleCmp, + canActivate: ['canActivate_alwaysTrueAndLogging'], + }, + ], + }, + ]); - router.navigateByUrl('/main/component1'); - advance(fixture); - expect(location.path()).toEqual('/main/component1'); + router.navigateByUrl('/main/component1'); + advance(fixture); + expect(location.path()).toEqual('/main/component1'); - router.navigateByUrl('/main/component2'); - advance(fixture); - expect(location.path()).toEqual('/main/component1'); - expect(log).toEqual(['called']); - }))); + router.navigateByUrl('/main/component2'); + advance(fixture); + expect(location.path()).toEqual('/main/component1'); + expect(log).toEqual(['called']); + }), + )); - it('should call guards every time when navigating to the same url over and over again', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should call guards every time when navigating to the same url over and over again', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'simple', component: SimpleCmp, canDeactivate: ['alwaysFalseAndLogging']}, - {path: 'blank', component: BlankCmp} + router.resetConfig([ + {path: 'simple', component: SimpleCmp, canDeactivate: ['alwaysFalseAndLogging']}, + {path: 'blank', component: BlankCmp}, + ]); - ]); + router.navigateByUrl('/simple'); + advance(fixture); - router.navigateByUrl('/simple'); - advance(fixture); + router.navigateByUrl('/blank'); + advance(fixture); + expect(log).toEqual(['called']); + expect(location.path()).toEqual('/simple'); - router.navigateByUrl('/blank'); - advance(fixture); - expect(log).toEqual(['called']); - expect(location.path()).toEqual('/simple'); - - router.navigateByUrl('/blank'); - advance(fixture); - expect(log).toEqual(['called', 'called']); - expect(location.path()).toEqual('/simple'); - }))); + router.navigateByUrl('/blank'); + advance(fixture); + expect(log).toEqual(['called', 'called']); + expect(location.path()).toEqual('/simple'); + }), + )); describe('next state', () => { let log: string[]; class ClassWithNextState { canDeactivate( - component: TeamCmp, currentRoute: ActivatedRouteSnapshot, - currentState: RouterStateSnapshot, nextState: RouterStateSnapshot): boolean { + component: TeamCmp, + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState: RouterStateSnapshot, + ): boolean { log.push(currentState.url, nextState.url); return true; } @@ -4353,60 +4700,77 @@ for (const browserAPI of ['navigation', 'history'] as const) { log = []; TestBed.configureTestingModule({ providers: [ - ClassWithNextState, { + ClassWithNextState, + { provide: 'FunctionWithNextState', - useValue: - (cmp: any, currentRoute: ActivatedRouteSnapshot, - currentState: RouterStateSnapshot, nextState: RouterStateSnapshot) => { - log.push(currentState.url, nextState.url); - return true; - } - } - ] + useValue: ( + cmp: any, + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState: RouterStateSnapshot, + ) => { + log.push(currentState.url, nextState.url); + return true; + }, + }, + ], }); }); - it('should pass next state as the 4 argument when guard is a class', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should pass next state as the 4 argument when guard is a class', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - canDeactivate: - [(component: TeamCmp, currentRoute: ActivatedRouteSnapshot, - currentState: RouterStateSnapshot, nextState: RouterStateSnapshot) => - coreInject(ClassWithNextState) - .canDeactivate(component, currentRoute, currentState, nextState)] - }]); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + canDeactivate: [ + ( + component: TeamCmp, + currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, + nextState: RouterStateSnapshot, + ) => + coreInject(ClassWithNextState).canDeactivate( + component, + currentRoute, + currentState, + nextState, + ), + ], + }, + ]); - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); - router.navigateByUrl('/team/33'); - advance(fixture); - expect(location.path()).toEqual('/team/33'); - expect(log).toEqual(['/team/22', '/team/33']); - }))); + router.navigateByUrl('/team/33'); + advance(fixture); + expect(location.path()).toEqual('/team/33'); + expect(log).toEqual(['/team/22', '/team/33']); + }), + )); - it('should pass next state as the 4 argument when guard is a function', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should pass next state as the 4 argument when guard is a function', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'team/:id', component: TeamCmp, canDeactivate: ['FunctionWithNextState']} - ]); + router.resetConfig([ + {path: 'team/:id', component: TeamCmp, canDeactivate: ['FunctionWithNextState']}, + ]); - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); - router.navigateByUrl('/team/33'); - advance(fixture); - expect(location.path()).toEqual('/team/33'); - expect(log).toEqual(['/team/22', '/team/33']); - }))); + router.navigateByUrl('/team/33'); + advance(fixture); + expect(location.path()).toEqual('/team/33'); + expect(log).toEqual(['/team/22', '/team/33']); + }), + )); }); describe('should work when given a class', () => { @@ -4420,54 +4784,62 @@ for (const browserAPI of ['navigation', 'history'] as const) { TestBed.configureTestingModule({providers: [AlwaysTrue]}); }); - it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('works', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - canDeactivate: [() => coreInject(AlwaysTrue).canDeactivate()] - }]); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + canDeactivate: [() => coreInject(AlwaysTrue).canDeactivate()], + }, + ]); - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); - router.navigateByUrl('/team/33'); - advance(fixture); - expect(location.path()).toEqual('/team/33'); - }))); + router.navigateByUrl('/team/33'); + advance(fixture); + expect(location.path()).toEqual('/team/33'); + }), + )); }); - describe('should work when returns an observable', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [{ - provide: 'CanDeactivate', - useValue: (c: TeamCmp, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { - return new Observable((observer) => { - observer.next(false); - }); - } - }] + providers: [ + { + provide: 'CanDeactivate', + useValue: (c: TeamCmp, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => { + return new Observable((observer) => { + observer.next(false); + }); + }, + }, + ], }); }); - it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('works', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig( - [{path: 'team/:id', component: TeamCmp, canDeactivate: ['CanDeactivate']}]); + router.resetConfig([ + {path: 'team/:id', component: TeamCmp, canDeactivate: ['CanDeactivate']}, + ]); - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); - router.navigateByUrl('/team/33'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); - }))); + router.navigateByUrl('/team/33'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); + }), + )); }); }); @@ -4475,70 +4847,80 @@ for (const browserAPI of ['navigation', 'history'] as const) { describe('should be invoked when activating a child', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [{ - provide: 'alwaysFalse', - useValue: (a: any, b: any) => a.paramMap.get('id') === '22', - }] + providers: [ + { + provide: 'alwaysFalse', + useValue: (a: any, b: any) => a.paramMap.get('id') === '22', + }, + ], }); }); - it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('works', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: '', - canActivateChild: ['alwaysFalse'], - children: [{path: 'team/:id', component: TeamCmp}] - }]); + router.resetConfig([ + { + path: '', + canActivateChild: ['alwaysFalse'], + children: [{path: 'team/:id', component: TeamCmp}], + }, + ]); - router.navigateByUrl('/team/22'); - advance(fixture); + router.navigateByUrl('/team/22'); + advance(fixture); - expect(location.path()).toEqual('/team/22'); + expect(location.path()).toEqual('/team/22'); - router.navigateByUrl('/team/33')!.catch(() => {}); - advance(fixture); + router.navigateByUrl('/team/33')!.catch(() => {}); + advance(fixture); - expect(location.path()).toEqual('/team/22'); - }))); + expect(location.path()).toEqual('/team/22'); + }), + )); }); - it('should find the guard provided in lazy loaded module', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - @Component({selector: 'admin', template: ''}) - class AdminComponent { - } + it('should find the guard provided in lazy loaded module', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + @Component({selector: 'admin', template: ''}) + class AdminComponent {} - @Component({selector: 'lazy', template: 'lazy-loaded'}) - class LazyLoadedComponent { - } + @Component({selector: 'lazy', template: 'lazy-loaded'}) + class LazyLoadedComponent {} - @NgModule({ - declarations: [AdminComponent, LazyLoadedComponent], - imports: [RouterModule.forChild([{ - path: '', - component: AdminComponent, - children: [{ - path: '', - canActivateChild: ['alwaysTrue'], - children: [{path: '', component: LazyLoadedComponent}] - }] - }])], - providers: [{provide: 'alwaysTrue', useValue: () => true}], - }) - class LazyLoadedModule { - } + @NgModule({ + declarations: [AdminComponent, LazyLoadedComponent], + imports: [ + RouterModule.forChild([ + { + path: '', + component: AdminComponent, + children: [ + { + path: '', + canActivateChild: ['alwaysTrue'], + children: [{path: '', component: LazyLoadedComponent}], + }, + ], + }, + ]), + ], + providers: [{provide: 'alwaysTrue', useValue: () => true}], + }) + class LazyLoadedModule {} - const fixture = createRoot(router, RootCmp); + const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'admin', loadChildren: () => LazyLoadedModule}]); + router.resetConfig([{path: 'admin', loadChildren: () => LazyLoadedModule}]); - router.navigateByUrl('/admin'); - advance(fixture); + router.navigateByUrl('/admin'); + advance(fixture); - expect(location.path()).toEqual('/admin'); - expect(fixture.nativeElement).toHaveText('lazy-loaded'); - }))); + expect(location.path()).toEqual('/admin'); + expect(fixture.nativeElement).toHaveText('lazy-loaded'); + }), + )); }); describe('CanLoad', () => { @@ -4568,256 +4950,273 @@ for (const browserAPI of ['navigation', 'history'] as const) { useValue: () => { canLoadRunCount++; return true; - } + }, }, - ] + ], }); }); - it('should not load children when CanLoad returns false', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - @Component({selector: 'lazy', template: 'lazy-loaded'}) - class LazyLoadedComponent { - } + it('should not load children when CanLoad returns false', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + @Component({selector: 'lazy', template: 'lazy-loaded'}) + class LazyLoadedComponent {} - @NgModule({ - declarations: [LazyLoadedComponent], - imports: [RouterModule.forChild([{path: 'loaded', component: LazyLoadedComponent}])] - }) - class LoadedModule { - } + @NgModule({ + declarations: [LazyLoadedComponent], + imports: [RouterModule.forChild([{path: 'loaded', component: LazyLoadedComponent}])], + }) + class LoadedModule {} - const fixture = createRoot(router, RootCmp); + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'lazyFalse', canLoad: ['alwaysFalse'], loadChildren: () => LoadedModule}, - {path: 'lazyTrue', canLoad: ['alwaysTrue'], loadChildren: () => LoadedModule} - ]); + router.resetConfig([ + {path: 'lazyFalse', canLoad: ['alwaysFalse'], loadChildren: () => LoadedModule}, + {path: 'lazyTrue', canLoad: ['alwaysTrue'], loadChildren: () => LoadedModule}, + ]); - const recordedEvents: Event[] = []; - router.events.forEach(e => recordedEvents.push(e)); + const recordedEvents: Event[] = []; + router.events.forEach((e) => recordedEvents.push(e)); + // failed navigation + router.navigateByUrl('/lazyFalse/loaded'); + advance(fixture); - // failed navigation - router.navigateByUrl('/lazyFalse/loaded'); - advance(fixture); + expect(location.path()).toEqual(''); - expect(location.path()).toEqual(''); + expectEvents(recordedEvents, [ + [NavigationStart, '/lazyFalse/loaded'], + // [GuardsCheckStart, '/lazyFalse/loaded'], + [NavigationCancel, '/lazyFalse/loaded'], + ]); - expectEvents(recordedEvents, [ - [NavigationStart, '/lazyFalse/loaded'], - // [GuardsCheckStart, '/lazyFalse/loaded'], - [NavigationCancel, '/lazyFalse/loaded'], - ]); + expect((recordedEvents[1] as NavigationCancel).code).toBe( + NavigationCancellationCode.GuardRejected, + ); - expect((recordedEvents[1] as NavigationCancel).code) - .toBe(NavigationCancellationCode.GuardRejected); + recordedEvents.splice(0); - recordedEvents.splice(0); + // successful navigation + router.navigateByUrl('/lazyTrue/loaded'); + advance(fixture); - // successful navigation - router.navigateByUrl('/lazyTrue/loaded'); - advance(fixture); + expect(location.path()).toEqual('/lazyTrue/loaded'); - expect(location.path()).toEqual('/lazyTrue/loaded'); + expectEvents(recordedEvents, [ + [NavigationStart, '/lazyTrue/loaded'], + [RouteConfigLoadStart], + [RouteConfigLoadEnd], + [RoutesRecognized, '/lazyTrue/loaded'], + [GuardsCheckStart, '/lazyTrue/loaded'], + [ChildActivationStart], + [ActivationStart], + [ChildActivationStart], + [ActivationStart], + [GuardsCheckEnd, '/lazyTrue/loaded'], + [ResolveStart, '/lazyTrue/loaded'], + [ResolveEnd, '/lazyTrue/loaded'], + [ActivationEnd], + [ChildActivationEnd], + [ActivationEnd], + [ChildActivationEnd], + [NavigationEnd, '/lazyTrue/loaded'], + ]); + }), + )); - expectEvents(recordedEvents, [ - [NavigationStart, '/lazyTrue/loaded'], - [RouteConfigLoadStart], - [RouteConfigLoadEnd], - [RoutesRecognized, '/lazyTrue/loaded'], - [GuardsCheckStart, '/lazyTrue/loaded'], - [ChildActivationStart], - [ActivationStart], - [ChildActivationStart], - [ActivationStart], - [GuardsCheckEnd, '/lazyTrue/loaded'], - [ResolveStart, '/lazyTrue/loaded'], - [ResolveEnd, '/lazyTrue/loaded'], - [ActivationEnd], - [ChildActivationEnd], - [ActivationEnd], - [ChildActivationEnd], - [NavigationEnd, '/lazyTrue/loaded'], - ]); - }))); + it('should support navigating from within the guard', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - it('should support navigating from within the guard', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + router.resetConfig([ + { + path: 'lazyFalse', + canLoad: ['returnFalseAndNavigate'], + loadChildren: jasmine.createSpy('lazyFalse'), + }, + {path: 'blank', component: BlankCmp}, + ]); - router.resetConfig([ - { - path: 'lazyFalse', - canLoad: ['returnFalseAndNavigate'], - loadChildren: jasmine.createSpy('lazyFalse') - }, - {path: 'blank', component: BlankCmp} - ]); + const recordedEvents: Event[] = []; + router.events.forEach((e) => recordedEvents.push(e)); - const recordedEvents: Event[] = []; - router.events.forEach(e => recordedEvents.push(e)); + router.navigateByUrl('/lazyFalse/loaded'); + advance(fixture); + expect(location.path()).toEqual('/blank'); - router.navigateByUrl('/lazyFalse/loaded'); - advance(fixture); + expectEvents(recordedEvents, [ + [NavigationStart, '/lazyFalse/loaded'], + // No GuardCheck events as `canLoad` is a special guard that's not actually part of + // the guard lifecycle. + [NavigationCancel, '/lazyFalse/loaded'], - expect(location.path()).toEqual('/blank'); + [NavigationStart, '/blank'], + [RoutesRecognized, '/blank'], + [GuardsCheckStart, '/blank'], + [ChildActivationStart], + [ActivationStart], + [GuardsCheckEnd, '/blank'], + [ResolveStart, '/blank'], + [ResolveEnd, '/blank'], + [ActivationEnd], + [ChildActivationEnd], + [NavigationEnd, '/blank'], + ]); - expectEvents(recordedEvents, [ - [NavigationStart, '/lazyFalse/loaded'], - // No GuardCheck events as `canLoad` is a special guard that's not actually part of - // the guard lifecycle. - [NavigationCancel, '/lazyFalse/loaded'], + expect((recordedEvents[1] as NavigationCancel).code).toBe( + NavigationCancellationCode.SupersededByNewNavigation, + ); + }), + )); - [NavigationStart, '/blank'], [RoutesRecognized, '/blank'], - [GuardsCheckStart, '/blank'], [ChildActivationStart], [ActivationStart], - [GuardsCheckEnd, '/blank'], [ResolveStart, '/blank'], [ResolveEnd, '/blank'], - [ActivationEnd], [ChildActivationEnd], [NavigationEnd, '/blank'] - ]); + it('should support returning UrlTree from within the guard', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - expect((recordedEvents[1] as NavigationCancel).code) - .toBe(NavigationCancellationCode.SupersededByNewNavigation); - }))); + router.resetConfig([ + { + path: 'lazyFalse', + canLoad: ['returnUrlTree'], + loadChildren: jasmine.createSpy('lazyFalse'), + }, + {path: 'blank', component: BlankCmp}, + ]); - it('should support returning UrlTree from within the guard', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + const recordedEvents: Event[] = []; + router.events.forEach((e) => recordedEvents.push(e)); - router.resetConfig([ - { - path: 'lazyFalse', - canLoad: ['returnUrlTree'], - loadChildren: jasmine.createSpy('lazyFalse') - }, - {path: 'blank', component: BlankCmp} - ]); + router.navigateByUrl('/lazyFalse/loaded'); + advance(fixture); - const recordedEvents: Event[] = []; - router.events.forEach(e => recordedEvents.push(e)); + expect(location.path()).toEqual('/blank'); + expectEvents(recordedEvents, [ + [NavigationStart, '/lazyFalse/loaded'], + // No GuardCheck events as `canLoad` is a special guard that's not actually part of + // the guard lifecycle. + [NavigationCancel, '/lazyFalse/loaded'], - router.navigateByUrl('/lazyFalse/loaded'); - advance(fixture); + [NavigationStart, '/blank'], + [RoutesRecognized, '/blank'], + [GuardsCheckStart, '/blank'], + [ChildActivationStart], + [ActivationStart], + [GuardsCheckEnd, '/blank'], + [ResolveStart, '/blank'], + [ResolveEnd, '/blank'], + [ActivationEnd], + [ChildActivationEnd], + [NavigationEnd, '/blank'], + ]); - expect(location.path()).toEqual('/blank'); - - expectEvents(recordedEvents, [ - [NavigationStart, '/lazyFalse/loaded'], - // No GuardCheck events as `canLoad` is a special guard that's not actually part of - // the guard lifecycle. - [NavigationCancel, '/lazyFalse/loaded'], - - [NavigationStart, '/blank'], [RoutesRecognized, '/blank'], - [GuardsCheckStart, '/blank'], [ChildActivationStart], [ActivationStart], - [GuardsCheckEnd, '/blank'], [ResolveStart, '/blank'], [ResolveEnd, '/blank'], - [ActivationEnd], [ChildActivationEnd], [NavigationEnd, '/blank'] - ]); - - expect((recordedEvents[1] as NavigationCancel).code) - .toBe(NavigationCancellationCode.Redirect); - }))); + expect((recordedEvents[1] as NavigationCancel).code).toBe( + NavigationCancellationCode.Redirect, + ); + }), + )); // Regression where navigateByUrl with false CanLoad no longer resolved `false` value on // navigateByUrl promise: https://github.com/angular/angular/issues/26284 - it('should resolve navigateByUrl promise after CanLoad executes', - fakeAsync(inject([Router], (router: Router) => { - @Component({selector: 'lazy', template: 'lazy-loaded'}) - class LazyLoadedComponent { - } + it('should resolve navigateByUrl promise after CanLoad executes', fakeAsync( + inject([Router], (router: Router) => { + @Component({selector: 'lazy', template: 'lazy-loaded'}) + class LazyLoadedComponent {} - @NgModule({ - declarations: [LazyLoadedComponent], - imports: [RouterModule.forChild([{path: 'loaded', component: LazyLoadedComponent}])] - }) - class LazyLoadedModule { - } + @NgModule({ + declarations: [LazyLoadedComponent], + imports: [RouterModule.forChild([{path: 'loaded', component: LazyLoadedComponent}])], + }) + class LazyLoadedModule {} - const fixture = createRoot(router, RootCmp); + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'lazy-false', canLoad: ['alwaysFalse'], loadChildren: () => LazyLoadedModule}, - {path: 'lazy-true', canLoad: ['alwaysTrue'], loadChildren: () => LazyLoadedModule}, - ]); + router.resetConfig([ + {path: 'lazy-false', canLoad: ['alwaysFalse'], loadChildren: () => LazyLoadedModule}, + {path: 'lazy-true', canLoad: ['alwaysTrue'], loadChildren: () => LazyLoadedModule}, + ]); - let navFalseResult = true; - let navTrueResult = false; - router.navigateByUrl('/lazy-false').then(v => { - navFalseResult = v; - }); - advance(fixture); - router.navigateByUrl('/lazy-true').then(v => { - navTrueResult = v; - }); - advance(fixture); + let navFalseResult = true; + let navTrueResult = false; + router.navigateByUrl('/lazy-false').then((v) => { + navFalseResult = v; + }); + advance(fixture); + router.navigateByUrl('/lazy-true').then((v) => { + navTrueResult = v; + }); + advance(fixture); - expect(navFalseResult).toBe(false); - expect(navTrueResult).toBe(true); - }))); + expect(navFalseResult).toBe(false); + expect(navTrueResult).toBe(true); + }), + )); - it('should execute CanLoad only once', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - @Component({selector: 'lazy', template: 'lazy-loaded'}) - class LazyLoadedComponent { - } + it('should execute CanLoad only once', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + @Component({selector: 'lazy', template: 'lazy-loaded'}) + class LazyLoadedComponent {} - @NgModule({ - declarations: [LazyLoadedComponent], - imports: [RouterModule.forChild([{path: 'loaded', component: LazyLoadedComponent}])] - }) - class LazyLoadedModule { - } + @NgModule({ + declarations: [LazyLoadedComponent], + imports: [RouterModule.forChild([{path: 'loaded', component: LazyLoadedComponent}])], + }) + class LazyLoadedModule {} - const fixture = createRoot(router, RootCmp); + const fixture = createRoot(router, RootCmp); - router.resetConfig( - [{path: 'lazy', canLoad: ['alwaysTrue'], loadChildren: () => LazyLoadedModule}]); + router.resetConfig([ + {path: 'lazy', canLoad: ['alwaysTrue'], loadChildren: () => LazyLoadedModule}, + ]); - router.navigateByUrl('/lazy/loaded'); - advance(fixture); - expect(location.path()).toEqual('/lazy/loaded'); - expect(canLoadRunCount).toEqual(1); + router.navigateByUrl('/lazy/loaded'); + advance(fixture); + expect(location.path()).toEqual('/lazy/loaded'); + expect(canLoadRunCount).toEqual(1); - router.navigateByUrl('/'); - advance(fixture); - expect(location.path()).toEqual(''); + router.navigateByUrl('/'); + advance(fixture); + expect(location.path()).toEqual(''); - router.navigateByUrl('/lazy/loaded'); - advance(fixture); - expect(location.path()).toEqual('/lazy/loaded'); - expect(canLoadRunCount).toEqual(1); - }))); + router.navigateByUrl('/lazy/loaded'); + advance(fixture); + expect(location.path()).toEqual('/lazy/loaded'); + expect(canLoadRunCount).toEqual(1); + }), + )); it('cancels guard execution when a new navigation happens', fakeAsync(() => { - @Injectable({providedIn: 'root'}) - class DelayedGuard { - static delayedExecutions = 0; - static canLoadCalls = 0; - canLoad() { - DelayedGuard.canLoadCalls++; - return of(true).pipe(delay(1000), tap(() => { - DelayedGuard.delayedExecutions++; - })); - } - } - const router = TestBed.inject(Router); - router.resetConfig([ - {path: 'a', canLoad: [DelayedGuard], loadChildren: () => [], component: SimpleCmp}, - {path: 'team/:id', component: TeamCmp}, - ]); - const fixture = createRoot(router, RootCmp); + @Injectable({providedIn: 'root'}) + class DelayedGuard { + static delayedExecutions = 0; + static canLoadCalls = 0; + canLoad() { + DelayedGuard.canLoadCalls++; + return of(true).pipe( + delay(1000), + tap(() => { + DelayedGuard.delayedExecutions++; + }), + ); + } + } + const router = TestBed.inject(Router); + router.resetConfig([ + {path: 'a', canLoad: [DelayedGuard], loadChildren: () => [], component: SimpleCmp}, + {path: 'team/:id', component: TeamCmp}, + ]); + const fixture = createRoot(router, RootCmp); - router.navigateByUrl('/a'); - tick(10); - // The delayed guard should have started - expect(DelayedGuard.canLoadCalls).toEqual(1); - router.navigateByUrl('/team/1'); - advance(fixture, 1000); - expect(fixture.nativeElement.innerHTML).toContain('team'); - // The delayed guard should not execute the delayed condition because a new navigation - // cancels the current one and unsubscribes from intermediate results. - expect(DelayedGuard.delayedExecutions).toEqual(0); - })); + router.navigateByUrl('/a'); + tick(10); + // The delayed guard should have started + expect(DelayedGuard.canLoadCalls).toEqual(1); + router.navigateByUrl('/team/1'); + advance(fixture, 1000); + expect(fixture.nativeElement.innerHTML).toContain('team'); + // The delayed guard should not execute the delayed condition because a new navigation + // cancels the current one and unsubscribes from intermediate results. + expect(DelayedGuard.delayedExecutions).toEqual(0); + })); }); describe('should run CanLoad guards concurrently', () => { @@ -4835,20 +5234,20 @@ for (const browserAPI of ['navigation', 'history'] as const) { provide: 'guard1', useValue: () => { return delayObservable(5).pipe(tap({next: () => log.push('guard1')})); - } + }, }, { provide: 'guard2', useValue: () => { return delayObservable(0).pipe(tap({next: () => log.push('guard2')})); - } + }, }, { provide: 'returnFalse', useValue: () => { log.push('returnFalse'); return false; - } + }, }, { provide: 'returnFalseAndNavigate', @@ -4857,96 +5256,103 @@ for (const browserAPI of ['navigation', 'history'] as const) { router.navigateByUrl('/redirected'); return false; }, - deps: [Router] + deps: [Router], }, { provide: 'returnUrlTree', useFactory: (router: Router) => () => { return delayObservable(15).pipe( - mapTo(router.parseUrl('/redirected')), - tap({next: () => log.push('returnUrlTree')})); + mapTo(router.parseUrl('/redirected')), + tap({next: () => log.push('returnUrlTree')}), + ); }, - deps: [Router] + deps: [Router], }, - ] + ], }); }); it('should only execute canLoad guards of routes being activated', fakeAsync(() => { - const router = TestBed.inject(Router); + const router = TestBed.inject(Router); - router.resetConfig([ - { - path: 'lazy', - canLoad: ['guard1'], - loadChildren: () => of(ModuleWithBlankCmpAsRoute) - }, - {path: 'redirected', component: SimpleCmp}, - // canLoad should not run for this route because 'lazy' activates first - { - path: '', - canLoad: ['returnFalseAndNavigate'], - loadChildren: () => of(ModuleWithBlankCmpAsRoute) - }, - ]); + router.resetConfig([ + { + path: 'lazy', + canLoad: ['guard1'], + loadChildren: () => of(ModuleWithBlankCmpAsRoute), + }, + {path: 'redirected', component: SimpleCmp}, + // canLoad should not run for this route because 'lazy' activates first + { + path: '', + canLoad: ['returnFalseAndNavigate'], + loadChildren: () => of(ModuleWithBlankCmpAsRoute), + }, + ]); - router.navigateByUrl('/lazy'); - tick(5); - expect(log.length).toEqual(1); - expect(log).toEqual(['guard1']); - })); + router.navigateByUrl('/lazy'); + tick(5); + expect(log.length).toEqual(1); + expect(log).toEqual(['guard1']); + })); - it('should execute canLoad guards', fakeAsync(inject([Router], (router: Router) => { - router.resetConfig([{ - path: 'lazy', - canLoad: ['guard1', 'guard2'], - loadChildren: () => ModuleWithBlankCmpAsRoute - }]); + it('should execute canLoad guards', fakeAsync( + inject([Router], (router: Router) => { + router.resetConfig([ + { + path: 'lazy', + canLoad: ['guard1', 'guard2'], + loadChildren: () => ModuleWithBlankCmpAsRoute, + }, + ]); - router.navigateByUrl('/lazy'); - tick(5); + router.navigateByUrl('/lazy'); + tick(5); - expect(log.length).toEqual(2); - expect(log).toEqual(['guard2', 'guard1']); - }))); + expect(log.length).toEqual(2); + expect(log).toEqual(['guard2', 'guard1']); + }), + )); - it('should redirect with UrlTree if higher priority guards have resolved', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - router.resetConfig([ - { - path: 'lazy', - canLoad: ['returnUrlTree', 'guard1', 'guard2'], - loadChildren: () => ModuleWithBlankCmpAsRoute - }, - {path: 'redirected', component: SimpleCmp} - ]); + it('should redirect with UrlTree if higher priority guards have resolved', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + router.resetConfig([ + { + path: 'lazy', + canLoad: ['returnUrlTree', 'guard1', 'guard2'], + loadChildren: () => ModuleWithBlankCmpAsRoute, + }, + {path: 'redirected', component: SimpleCmp}, + ]); - router.navigateByUrl('/lazy'); - tick(15); + router.navigateByUrl('/lazy'); + tick(15); - expect(log.length).toEqual(3); - expect(log).toEqual(['guard2', 'guard1', 'returnUrlTree']); - expect(location.path()).toEqual('/redirected'); - }))); + expect(log.length).toEqual(3); + expect(log).toEqual(['guard2', 'guard1', 'returnUrlTree']); + expect(location.path()).toEqual('/redirected'); + }), + )); - it('should redirect with UrlTree if UrlTree is lower priority', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - router.resetConfig([ - { - path: 'lazy', - canLoad: ['guard1', 'returnUrlTree'], - loadChildren: () => ModuleWithBlankCmpAsRoute - }, - {path: 'redirected', component: SimpleCmp} - ]); + it('should redirect with UrlTree if UrlTree is lower priority', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + router.resetConfig([ + { + path: 'lazy', + canLoad: ['guard1', 'returnUrlTree'], + loadChildren: () => ModuleWithBlankCmpAsRoute, + }, + {path: 'redirected', component: SimpleCmp}, + ]); - router.navigateByUrl('/lazy'); - tick(15); + router.navigateByUrl('/lazy'); + tick(15); - expect(log.length).toEqual(2); - expect(log).toEqual(['guard1', 'returnUrlTree']); - expect(location.path()).toEqual('/redirected'); - }))); + expect(log.length).toEqual(2); + expect(log).toEqual(['guard1', 'returnUrlTree']); + expect(location.path()).toEqual('/redirected'); + }), + )); }); describe('order', () => { @@ -4960,1215 +5366,1303 @@ for (const browserAPI of ['navigation', 'history'] as const) { beforeEach(() => { TestBed.configureTestingModule({ providers: [ - Logger, { + Logger, + { provide: 'canActivateChild_parent', useFactory: (logger: Logger) => () => (logger.add('canActivateChild_parent'), true), - deps: [Logger] + deps: [Logger], }, { provide: 'canActivate_team', useFactory: (logger: Logger) => () => (logger.add('canActivate_team'), true), - deps: [Logger] + deps: [Logger], }, { provide: 'canDeactivate_team', useFactory: (logger: Logger) => () => (logger.add('canDeactivate_team'), true), - deps: [Logger] + deps: [Logger], }, { provide: 'canDeactivate_simple', useFactory: (logger: Logger) => () => (logger.add('canDeactivate_simple'), true), - deps: [Logger] - } - ] + deps: [Logger], + }, + ], }); }); - it('should call guards in the right order', - fakeAsync(inject( - [Router, Location, Logger], (router: Router, location: Location, logger: Logger) => { - const fixture = createRoot(router, RootCmp); + it('should call guards in the right order', fakeAsync( + inject( + [Router, Location, Logger], + (router: Router, location: Location, logger: Logger) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: '', - canActivateChild: ['canActivateChild_parent'], - children: [{ - path: 'team/:id', - canActivate: ['canActivate_team'], - canDeactivate: ['canDeactivate_team'], - component: TeamCmp - }] - }]); + router.resetConfig([ + { + path: '', + canActivateChild: ['canActivateChild_parent'], + children: [ + { + path: 'team/:id', + canActivate: ['canActivate_team'], + canDeactivate: ['canDeactivate_team'], + component: TeamCmp, + }, + ], + }, + ]); - router.navigateByUrl('/team/22'); - advance(fixture); + router.navigateByUrl('/team/22'); + advance(fixture); - router.navigateByUrl('/team/33'); - advance(fixture); + router.navigateByUrl('/team/33'); + advance(fixture); - expect(logger.logs).toEqual([ - 'canActivateChild_parent', 'canActivate_team', + expect(logger.logs).toEqual([ + 'canActivateChild_parent', + 'canActivate_team', - 'canDeactivate_team', 'canActivateChild_parent', 'canActivate_team' - ]); - }))); + 'canDeactivate_team', + 'canActivateChild_parent', + 'canActivate_team', + ]); + }, + ), + )); - it('should call deactivate guards from bottom to top', - fakeAsync(inject( - [Router, Location, Logger], (router: Router, location: Location, logger: Logger) => { - const fixture = createRoot(router, RootCmp); + it('should call deactivate guards from bottom to top', fakeAsync( + inject( + [Router, Location, Logger], + (router: Router, location: Location, logger: Logger) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: '', - children: [{ - path: 'team/:id', - canDeactivate: ['canDeactivate_team'], - children: [ - {path: '', component: SimpleCmp, canDeactivate: ['canDeactivate_simple']} - ], - component: TeamCmp - }] - }]); + router.resetConfig([ + { + path: '', + children: [ + { + path: 'team/:id', + canDeactivate: ['canDeactivate_team'], + children: [ + {path: '', component: SimpleCmp, canDeactivate: ['canDeactivate_simple']}, + ], + component: TeamCmp, + }, + ], + }, + ]); - router.navigateByUrl('/team/22'); - advance(fixture); + router.navigateByUrl('/team/22'); + advance(fixture); - router.navigateByUrl('/team/33'); - advance(fixture); + router.navigateByUrl('/team/33'); + advance(fixture); - expect(logger.logs).toEqual(['canDeactivate_simple', 'canDeactivate_team']); - }))); + expect(logger.logs).toEqual(['canDeactivate_simple', 'canDeactivate_team']); + }, + ), + )); }); describe('canMatch', () => { @Injectable({providedIn: 'root'}) class ConfigurableGuard { - result: Promise|Observable|boolean|UrlTree = false; + result: Promise | Observable | boolean | UrlTree = + false; canMatch() { return this.result; } } it('falls back to second route when canMatch returns false', fakeAsync(() => { - const router = TestBed.inject(Router); - router.resetConfig([ - { - path: 'a', - canMatch: [() => coreInject(ConfigurableGuard).canMatch()], - component: BlankCmp - }, - {path: 'a', component: SimpleCmp}, - ]); - const fixture = createRoot(router, RootCmp); + const router = TestBed.inject(Router); + router.resetConfig([ + { + path: 'a', + canMatch: [() => coreInject(ConfigurableGuard).canMatch()], + component: BlankCmp, + }, + {path: 'a', component: SimpleCmp}, + ]); + const fixture = createRoot(router, RootCmp); - router.navigateByUrl('/a'); - advance(fixture); - expect(fixture.nativeElement.innerHTML).toContain('simple'); - })); + router.navigateByUrl('/a'); + advance(fixture); + expect(fixture.nativeElement.innerHTML).toContain('simple'); + })); it('uses route when canMatch returns true', fakeAsync(() => { - const router = TestBed.inject(Router); - TestBed.inject(ConfigurableGuard).result = Promise.resolve(true); - router.resetConfig([ - { - path: 'a', - canMatch: [() => coreInject(ConfigurableGuard).canMatch()], - component: SimpleCmp - }, - {path: 'a', component: BlankCmp}, - ]); - const fixture = createRoot(router, RootCmp); + const router = TestBed.inject(Router); + TestBed.inject(ConfigurableGuard).result = Promise.resolve(true); + router.resetConfig([ + { + path: 'a', + canMatch: [() => coreInject(ConfigurableGuard).canMatch()], + component: SimpleCmp, + }, + {path: 'a', component: BlankCmp}, + ]); + const fixture = createRoot(router, RootCmp); - - router.navigateByUrl('/a'); - advance(fixture); - expect(fixture.nativeElement.innerHTML).toContain('simple'); - })); + router.navigateByUrl('/a'); + advance(fixture); + expect(fixture.nativeElement.innerHTML).toContain('simple'); + })); it('can return UrlTree from canMatch guard', fakeAsync(() => { - const router = TestBed.inject(Router); - TestBed.inject(ConfigurableGuard).result = - Promise.resolve(router.createUrlTree(['/team/1'])); - router.resetConfig([ - { - path: 'a', - canMatch: [() => coreInject(ConfigurableGuard).canMatch()], - component: SimpleCmp - }, - {path: 'team/:id', component: TeamCmp}, - ]); - const fixture = createRoot(router, RootCmp); + const router = TestBed.inject(Router); + TestBed.inject(ConfigurableGuard).result = Promise.resolve( + router.createUrlTree(['/team/1']), + ); + router.resetConfig([ + { + path: 'a', + canMatch: [() => coreInject(ConfigurableGuard).canMatch()], + component: SimpleCmp, + }, + {path: 'team/:id', component: TeamCmp}, + ]); + const fixture = createRoot(router, RootCmp); - - router.navigateByUrl('/a'); - advance(fixture); - expect(fixture.nativeElement.innerHTML).toContain('team'); - })); + router.navigateByUrl('/a'); + advance(fixture); + expect(fixture.nativeElement.innerHTML).toContain('team'); + })); it('can return UrlTree from CanMatchFn guard', fakeAsync(() => { - const canMatchTeamSection = new InjectionToken('CanMatchTeamSection'); - const canMatchFactory: (router: Router) => CanMatchFn = (router: Router) => () => - router.createUrlTree(['/team/1']); + const canMatchTeamSection = new InjectionToken('CanMatchTeamSection'); + const canMatchFactory: (router: Router) => CanMatchFn = (router: Router) => () => + router.createUrlTree(['/team/1']); - TestBed.overrideProvider( - canMatchTeamSection, {useFactory: canMatchFactory, deps: [Router]}); + TestBed.overrideProvider(canMatchTeamSection, { + useFactory: canMatchFactory, + deps: [Router], + }); - const router = TestBed.inject(Router); + const router = TestBed.inject(Router); - router.resetConfig([ - {path: 'a', canMatch: [canMatchTeamSection], component: SimpleCmp}, - {path: 'team/:id', component: TeamCmp}, - ]); - const fixture = createRoot(router, RootCmp); + router.resetConfig([ + {path: 'a', canMatch: [canMatchTeamSection], component: SimpleCmp}, + {path: 'team/:id', component: TeamCmp}, + ]); + const fixture = createRoot(router, RootCmp); - - router.navigateByUrl('/a'); - advance(fixture); - expect(fixture.nativeElement.innerHTML).toContain('team'); - })); + router.navigateByUrl('/a'); + advance(fixture); + expect(fixture.nativeElement.innerHTML).toContain('team'); + })); it('runs canMatch guards provided in lazy module', fakeAsync(() => { - const router = TestBed.inject(Router); - @Component({ - selector: 'lazy', - template: 'lazy-loaded-parent []' - }) - class ParentLazyLoadedComponent { - } + const router = TestBed.inject(Router); + @Component({ + selector: 'lazy', + template: 'lazy-loaded-parent []', + }) + class ParentLazyLoadedComponent {} - @Component({selector: 'lazy', template: 'lazy-loaded-child'}) - class ChildLazyLoadedComponent { - } - @Injectable() - class LazyCanMatchFalse { - canMatch() { - return false; - } - } - @Component({template: 'restricted'}) - class Restricted { - } - @NgModule({ - declarations: [ParentLazyLoadedComponent, ChildLazyLoadedComponent, Restricted], - providers: [LazyCanMatchFalse], - imports: [RouterModule.forChild([ - { - path: 'loaded', - canMatch: [LazyCanMatchFalse], - component: Restricted, - children: [{path: 'child', component: Restricted}], - }, - { - path: 'loaded', - component: ParentLazyLoadedComponent, - children: [{path: 'child', component: ChildLazyLoadedComponent}] - } - ])] - }) - class LoadedModule { - } + @Component({selector: 'lazy', template: 'lazy-loaded-child'}) + class ChildLazyLoadedComponent {} + @Injectable() + class LazyCanMatchFalse { + canMatch() { + return false; + } + } + @Component({template: 'restricted'}) + class Restricted {} + @NgModule({ + declarations: [ParentLazyLoadedComponent, ChildLazyLoadedComponent, Restricted], + providers: [LazyCanMatchFalse], + imports: [ + RouterModule.forChild([ + { + path: 'loaded', + canMatch: [LazyCanMatchFalse], + component: Restricted, + children: [{path: 'child', component: Restricted}], + }, + { + path: 'loaded', + component: ParentLazyLoadedComponent, + children: [{path: 'child', component: ChildLazyLoadedComponent}], + }, + ]), + ], + }) + class LoadedModule {} + const fixture = createRoot(router, RootCmp); - const fixture = createRoot(router, RootCmp); + router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); + router.navigateByUrl('/lazy/loaded/child'); + advance(fixture); - router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); - router.navigateByUrl('/lazy/loaded/child'); - advance(fixture); - - expect(TestBed.inject(Location).path()).toEqual('/lazy/loaded/child'); - expect(fixture.nativeElement).toHaveText('lazy-loaded-parent [lazy-loaded-child]'); - })); + expect(TestBed.inject(Location).path()).toEqual('/lazy/loaded/child'); + expect(fixture.nativeElement).toHaveText('lazy-loaded-parent [lazy-loaded-child]'); + })); it('cancels guard execution when a new navigation happens', fakeAsync(() => { - @Injectable({providedIn: 'root'}) - class DelayedGuard { - static delayedExecutions = 0; - canMatch() { - return of(true).pipe(delay(1000), tap(() => { - DelayedGuard.delayedExecutions++; - })); - } - } - const router = TestBed.inject(Router); - const delayedGuardSpy = spyOn(TestBed.inject(DelayedGuard), 'canMatch'); - delayedGuardSpy.and.callThrough(); - const configurableMatchSpy = spyOn(TestBed.inject(ConfigurableGuard), 'canMatch'); - configurableMatchSpy.and.callFake(() => { - router.navigateByUrl('/team/1'); - return false; - }); - router.resetConfig([ - {path: 'a', canMatch: [ConfigurableGuard, DelayedGuard], component: SimpleCmp}, - {path: 'a', canMatch: [ConfigurableGuard, DelayedGuard], component: SimpleCmp}, - {path: 'a', canMatch: [ConfigurableGuard, DelayedGuard], component: SimpleCmp}, - {path: 'team/:id', component: TeamCmp}, - ]); - const fixture = createRoot(router, RootCmp); + @Injectable({providedIn: 'root'}) + class DelayedGuard { + static delayedExecutions = 0; + canMatch() { + return of(true).pipe( + delay(1000), + tap(() => { + DelayedGuard.delayedExecutions++; + }), + ); + } + } + const router = TestBed.inject(Router); + const delayedGuardSpy = spyOn(TestBed.inject(DelayedGuard), 'canMatch'); + delayedGuardSpy.and.callThrough(); + const configurableMatchSpy = spyOn(TestBed.inject(ConfigurableGuard), 'canMatch'); + configurableMatchSpy.and.callFake(() => { + router.navigateByUrl('/team/1'); + return false; + }); + router.resetConfig([ + {path: 'a', canMatch: [ConfigurableGuard, DelayedGuard], component: SimpleCmp}, + {path: 'a', canMatch: [ConfigurableGuard, DelayedGuard], component: SimpleCmp}, + {path: 'a', canMatch: [ConfigurableGuard, DelayedGuard], component: SimpleCmp}, + {path: 'team/:id', component: TeamCmp}, + ]); + const fixture = createRoot(router, RootCmp); + router.navigateByUrl('/a'); + advance(fixture); + expect(fixture.nativeElement.innerHTML).toContain('team'); - router.navigateByUrl('/a'); - advance(fixture); - expect(fixture.nativeElement.innerHTML).toContain('team'); + expect(configurableMatchSpy.calls.count()).toEqual(1); - expect(configurableMatchSpy.calls.count()).toEqual(1); - - // The delayed guard should not execute the delayed condition because the other guard - // initiates a new navigation, which cancels the current one and unsubscribes from - // intermediate results. - expect(DelayedGuard.delayedExecutions).toEqual(0); - // The delayed guard should still have executed once because guards are executed at the - // same time - expect(delayedGuardSpy.calls.count()).toEqual(1); - })); + // The delayed guard should not execute the delayed condition because the other guard + // initiates a new navigation, which cancels the current one and unsubscribes from + // intermediate results. + expect(DelayedGuard.delayedExecutions).toEqual(0); + // The delayed guard should still have executed once because guards are executed at the + // same time + expect(delayedGuardSpy.calls.count()).toEqual(1); + })); }); it('should allow guards as functions', fakeAsync(() => { - @Component({ - template: '', - standalone: true, - }) - class BlankCmp { - } - const router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmp); - const guards = { - canActivate() { - return true; - }, - canDeactivate() { - return true; - }, - canActivateChild() { - return true; - }, - canMatch() { - return true; - }, - canLoad() { - return true; - } - }; - spyOn(guards, 'canActivate').and.callThrough(); - spyOn(guards, 'canActivateChild').and.callThrough(); - spyOn(guards, 'canDeactivate').and.callThrough(); - spyOn(guards, 'canLoad').and.callThrough(); - spyOn(guards, 'canMatch').and.callThrough(); - router.resetConfig([ - { - path: '', - component: BlankCmp, - loadChildren: () => [{path: '', component: BlankCmp}], - canActivate: [guards.canActivate], - canActivateChild: [guards.canActivateChild], - canLoad: [guards.canLoad], - canDeactivate: [guards.canDeactivate], - canMatch: [guards.canMatch], - }, - { - path: 'other', - component: BlankCmp, - } - ]); + @Component({ + template: '', + standalone: true, + }) + class BlankCmp {} + const router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmp); + const guards = { + canActivate() { + return true; + }, + canDeactivate() { + return true; + }, + canActivateChild() { + return true; + }, + canMatch() { + return true; + }, + canLoad() { + return true; + }, + }; + spyOn(guards, 'canActivate').and.callThrough(); + spyOn(guards, 'canActivateChild').and.callThrough(); + spyOn(guards, 'canDeactivate').and.callThrough(); + spyOn(guards, 'canLoad').and.callThrough(); + spyOn(guards, 'canMatch').and.callThrough(); + router.resetConfig([ + { + path: '', + component: BlankCmp, + loadChildren: () => [{path: '', component: BlankCmp}], + canActivate: [guards.canActivate], + canActivateChild: [guards.canActivateChild], + canLoad: [guards.canLoad], + canDeactivate: [guards.canDeactivate], + canMatch: [guards.canMatch], + }, + { + path: 'other', + component: BlankCmp, + }, + ]); - router.navigateByUrl('/'); - advance(fixture); - expect(guards.canMatch).toHaveBeenCalled(); - expect(guards.canLoad).toHaveBeenCalled(); - expect(guards.canActivate).toHaveBeenCalled(); - expect(guards.canActivateChild).toHaveBeenCalled(); + router.navigateByUrl('/'); + advance(fixture); + expect(guards.canMatch).toHaveBeenCalled(); + expect(guards.canLoad).toHaveBeenCalled(); + expect(guards.canActivate).toHaveBeenCalled(); + expect(guards.canActivateChild).toHaveBeenCalled(); - router.navigateByUrl('/other'); - advance(fixture); - expect(guards.canDeactivate).toHaveBeenCalled(); - })); + router.navigateByUrl('/other'); + advance(fixture); + expect(guards.canDeactivate).toHaveBeenCalled(); + })); it('should allow DI in plain function guards', fakeAsync(() => { - @Component({ - template: '', - standalone: true, - }) - class BlankCmp { - } + @Component({ + template: '', + standalone: true, + }) + class BlankCmp {} - @Injectable({providedIn: 'root'}) - class State { - value = true; - } - const router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmp); - const guards = { - canActivate() { - return coreInject(State).value; - }, - canDeactivate() { - return coreInject(State).value; - }, - canActivateChild() { - return coreInject(State).value; - }, - canMatch() { - return coreInject(State).value; - }, - canLoad() { - return coreInject(State).value; - } - }; - spyOn(guards, 'canActivate').and.callThrough(); - spyOn(guards, 'canActivateChild').and.callThrough(); - spyOn(guards, 'canDeactivate').and.callThrough(); - spyOn(guards, 'canLoad').and.callThrough(); - spyOn(guards, 'canMatch').and.callThrough(); - router.resetConfig([ - { - path: '', - component: BlankCmp, - loadChildren: () => [{path: '', component: BlankCmp}], - canActivate: [guards.canActivate], - canActivateChild: [guards.canActivateChild], - canLoad: [guards.canLoad], - canDeactivate: [guards.canDeactivate], - canMatch: [guards.canMatch], - }, - { - path: 'other', - component: BlankCmp, - } - ]); + @Injectable({providedIn: 'root'}) + class State { + value = true; + } + const router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmp); + const guards = { + canActivate() { + return coreInject(State).value; + }, + canDeactivate() { + return coreInject(State).value; + }, + canActivateChild() { + return coreInject(State).value; + }, + canMatch() { + return coreInject(State).value; + }, + canLoad() { + return coreInject(State).value; + }, + }; + spyOn(guards, 'canActivate').and.callThrough(); + spyOn(guards, 'canActivateChild').and.callThrough(); + spyOn(guards, 'canDeactivate').and.callThrough(); + spyOn(guards, 'canLoad').and.callThrough(); + spyOn(guards, 'canMatch').and.callThrough(); + router.resetConfig([ + { + path: '', + component: BlankCmp, + loadChildren: () => [{path: '', component: BlankCmp}], + canActivate: [guards.canActivate], + canActivateChild: [guards.canActivateChild], + canLoad: [guards.canLoad], + canDeactivate: [guards.canDeactivate], + canMatch: [guards.canMatch], + }, + { + path: 'other', + component: BlankCmp, + }, + ]); - router.navigateByUrl('/'); - advance(fixture); - expect(guards.canMatch).toHaveBeenCalled(); - expect(guards.canLoad).toHaveBeenCalled(); - expect(guards.canActivate).toHaveBeenCalled(); - expect(guards.canActivateChild).toHaveBeenCalled(); + router.navigateByUrl('/'); + advance(fixture); + expect(guards.canMatch).toHaveBeenCalled(); + expect(guards.canLoad).toHaveBeenCalled(); + expect(guards.canActivate).toHaveBeenCalled(); + expect(guards.canActivateChild).toHaveBeenCalled(); - router.navigateByUrl('/other'); - advance(fixture); - expect(guards.canDeactivate).toHaveBeenCalled(); - })); + router.navigateByUrl('/other'); + advance(fixture); + expect(guards.canDeactivate).toHaveBeenCalled(); + })); it('can run functional guards serially', fakeAsync(() => { - function runSerially(guards: CanActivateFn[]|CanActivateChildFn[]): CanActivateFn| - CanActivateChildFn { - return (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { - const injector = coreInject(EnvironmentInjector); - const observables = guards.map(guard => { - const guardResult = injector.runInContext(() => guard(route, state)); - return wrapIntoObservable(guardResult).pipe(first()); - }); - return concat(...observables).pipe(takeWhile(v => v === true), last()); - }; - } + function runSerially( + guards: CanActivateFn[] | CanActivateChildFn[], + ): CanActivateFn | CanActivateChildFn { + return (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { + const injector = coreInject(EnvironmentInjector); + const observables = guards.map((guard) => { + const guardResult = injector.runInContext(() => guard(route, state)); + return wrapIntoObservable(guardResult).pipe(first()); + }); + return concat(...observables).pipe( + takeWhile((v) => v === true), + last(), + ); + }; + } - const guardDone: string[] = []; + const guardDone: string[] = []; - const guard1: CanActivateFn = () => - of(true).pipe(delay(100), tap(() => guardDone.push('guard1'))); - const guard2: CanActivateFn = () => of(true).pipe(tap(() => guardDone.push('guard2'))); - const guard3: CanActivateFn = () => - of(true).pipe(delay(50), tap(() => guardDone.push('guard3'))); - const guard4: CanActivateFn = () => - of(true).pipe(delay(200), tap(() => guardDone.push('guard4'))); - const router = TestBed.inject(Router); - router.resetConfig([{ - path: '**', - component: BlankCmp, - canActivate: [runSerially([guard1, guard2, guard3, guard4])] - }]); - router.navigateByUrl(''); + const guard1: CanActivateFn = () => + of(true).pipe( + delay(100), + tap(() => guardDone.push('guard1')), + ); + const guard2: CanActivateFn = () => of(true).pipe(tap(() => guardDone.push('guard2'))); + const guard3: CanActivateFn = () => + of(true).pipe( + delay(50), + tap(() => guardDone.push('guard3')), + ); + const guard4: CanActivateFn = () => + of(true).pipe( + delay(200), + tap(() => guardDone.push('guard4')), + ); + const router = TestBed.inject(Router); + router.resetConfig([ + { + path: '**', + component: BlankCmp, + canActivate: [runSerially([guard1, guard2, guard3, guard4])], + }, + ]); + router.navigateByUrl(''); - tick(100); - expect(guardDone).toEqual(['guard1', 'guard2']); - tick(50); - expect(guardDone).toEqual(['guard1', 'guard2', 'guard3']); - tick(200); - expect(guardDone).toEqual(['guard1', 'guard2', 'guard3', 'guard4']); - })); + tick(100); + expect(guardDone).toEqual(['guard1', 'guard2']); + tick(50); + expect(guardDone).toEqual(['guard1', 'guard2', 'guard3']); + tick(200); + expect(guardDone).toEqual(['guard1', 'guard2', 'guard3', 'guard4']); + })); }); describe('route events', () => { - it('should fire matching (Child)ActivationStart/End events', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should fire matching (Child)ActivationStart/End events', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'user/:name', component: UserCmp}]); + router.resetConfig([{path: 'user/:name', component: UserCmp}]); - const recordedEvents: Event[] = []; - router.events.forEach(e => recordedEvents.push(e)); + const recordedEvents: Event[] = []; + router.events.forEach((e) => recordedEvents.push(e)); - router.navigateByUrl('/user/fedor'); - advance(fixture); + router.navigateByUrl('/user/fedor'); + advance(fixture); - const event3 = recordedEvents[3] as ChildActivationStart; - const event9 = recordedEvents[9] as ChildActivationEnd; + const event3 = recordedEvents[3] as ChildActivationStart; + const event9 = recordedEvents[9] as ChildActivationEnd; - expect(fixture.nativeElement).toHaveText('user fedor'); - expect(event3 instanceof ChildActivationStart).toBe(true); - expect(event3.snapshot).toBe(event9.snapshot.root); - expect(event9 instanceof ChildActivationEnd).toBe(true); - expect(event9.snapshot).toBe(event9.snapshot.root); + expect(fixture.nativeElement).toHaveText('user fedor'); + expect(event3 instanceof ChildActivationStart).toBe(true); + expect(event3.snapshot).toBe(event9.snapshot.root); + expect(event9 instanceof ChildActivationEnd).toBe(true); + expect(event9.snapshot).toBe(event9.snapshot.root); - const event4 = recordedEvents[4] as ActivationStart; - const event8 = recordedEvents[8] as ActivationEnd; + const event4 = recordedEvents[4] as ActivationStart; + const event8 = recordedEvents[8] as ActivationEnd; - expect(event4 instanceof ActivationStart).toBe(true); - expect(event4.snapshot.routeConfig?.path).toBe('user/:name'); - expect(event8 instanceof ActivationEnd).toBe(true); - expect(event8.snapshot.routeConfig?.path).toBe('user/:name'); + expect(event4 instanceof ActivationStart).toBe(true); + expect(event4.snapshot.routeConfig?.path).toBe('user/:name'); + expect(event8 instanceof ActivationEnd).toBe(true); + expect(event8.snapshot.routeConfig?.path).toBe('user/:name'); - expectEvents(recordedEvents, [ - [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'] - ]); - }))); + expectEvents(recordedEvents, [ + [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 allow redirection in NavigationStart', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + it('should allow redirection in NavigationStart', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'blank', component: UserCmp}, - {path: 'user/:name', component: BlankCmp}, - ]); + router.resetConfig([ + {path: 'blank', component: UserCmp}, + {path: 'user/:name', component: BlankCmp}, + ]); - const navigateSpy = spyOn(router, 'navigate').and.callThrough(); - const recordedEvents: Event[] = []; + const navigateSpy = spyOn(router, 'navigate').and.callThrough(); + const recordedEvents: Event[] = []; - const navStart$ = router.events.pipe( - tap(e => recordedEvents.push(e)), - filter((e): e is NavigationStart => e instanceof NavigationStart), first()); + const navStart$ = router.events.pipe( + tap((e) => recordedEvents.push(e)), + filter((e): e is NavigationStart => e instanceof NavigationStart), + first(), + ); - navStart$.subscribe((e: NavigationStart|NavigationError) => { - router.navigate( - ['/blank'], {queryParams: {state: 'redirected'}, queryParamsHandling: 'merge'}); - advance(fixture); - }); + navStart$.subscribe((e: NavigationStart | NavigationError) => { + router.navigate(['/blank'], { + queryParams: {state: 'redirected'}, + queryParamsHandling: 'merge', + }); + advance(fixture); + }); - router.navigate(['/user/:fedor']); - advance(fixture); + router.navigate(['/user/:fedor']); + advance(fixture); - expect(navigateSpy.calls.mostRecent().args[1]!.queryParams); - }))); + expect(navigateSpy.calls.mostRecent().args[1]!.queryParams); + }), + )); + it('should stop emitting events after the router is destroyed', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); + router.resetConfig([{path: 'user/:name', component: UserCmp}]); - it('should stop emitting events after the router is destroyed', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'user/:name', component: UserCmp}]); + let events = 0; + const subscription = router.events.subscribe(() => events++); - let events = 0; - const subscription = router.events.subscribe(() => events++); + router.navigateByUrl('/user/frodo'); + advance(fixture); + expect(events).toBeGreaterThan(0); - router.navigateByUrl('/user/frodo'); - advance(fixture); - expect(events).toBeGreaterThan(0); + const previousCount = events; + router.dispose(); + router.navigateByUrl('/user/bilbo'); + advance(fixture); - const previousCount = events; - router.dispose(); - router.navigateByUrl('/user/bilbo'); - advance(fixture); + expect(events).toBe(previousCount); + subscription.unsubscribe(); + }), + )); - expect(events).toBe(previousCount); - subscription.unsubscribe(); - }))); + it('should resolve navigation promise with false after the router is destroyed', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); + let result = null as boolean | null; + const callback = (r: boolean) => (result = r); + router.resetConfig([{path: 'user/:name', component: UserCmp}]); - it('should resolve navigation promise with false after the router is destroyed', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); - let result = null as boolean | null; - const callback = (r: boolean) => result = r; - router.resetConfig([{path: 'user/:name', component: UserCmp}]); + router.navigateByUrl('/user/frodo').then(callback); + advance(fixture); + expect(result).toBe(true); + result = null as boolean | null; - router.navigateByUrl('/user/frodo').then(callback); - advance(fixture); - expect(result).toBe(true); - result = null as boolean | null; + router.dispose(); - router.dispose(); + router.navigateByUrl('/user/bilbo').then(callback); + advance(fixture); + expect(result).toBe(false); + result = null as boolean | null; - router.navigateByUrl('/user/bilbo').then(callback); - advance(fixture); - expect(result).toBe(false); - result = null as boolean | null; - - router.navigate(['/user/bilbo']).then(callback); - advance(fixture); - expect(result).toBe(false); - }))); + router.navigate(['/user/bilbo']).then(callback); + advance(fixture); + expect(result).toBe(false); + }), + )); }); describe('routerLinkActive', () => { - it('should set the class when the link is active (a tag)', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should set the class when the link is active (a tag)', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [{ - path: 'link', - component: DummyLinkCmp, - children: [{path: 'simple', component: SimpleCmp}, {path: '', component: BlankCmp}] - }] - }]); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + { + path: 'link', + component: DummyLinkCmp, + children: [ + {path: 'simple', component: SimpleCmp}, + {path: '', component: BlankCmp}, + ], + }, + ], + }, + ]); - router.navigateByUrl('/team/22/link;exact=true'); - advance(fixture); - advance(fixture); - expect(location.path()).toEqual('/team/22/link;exact=true'); + router.navigateByUrl('/team/22/link;exact=true'); + advance(fixture); + advance(fixture); + expect(location.path()).toEqual('/team/22/link;exact=true'); - const nativeLink = fixture.nativeElement.querySelector('a'); - const nativeButton = fixture.nativeElement.querySelector('button'); - expect(nativeLink.className).toEqual('active'); - expect(nativeButton.className).toEqual('active'); + const nativeLink = fixture.nativeElement.querySelector('a'); + const nativeButton = fixture.nativeElement.querySelector('button'); + expect(nativeLink.className).toEqual('active'); + expect(nativeButton.className).toEqual('active'); - router.navigateByUrl('/team/22/link/simple'); - advance(fixture); - expect(location.path()).toEqual('/team/22/link/simple'); - expect(nativeLink.className).toEqual(''); - expect(nativeButton.className).toEqual(''); - }))); + router.navigateByUrl('/team/22/link/simple'); + advance(fixture); + expect(location.path()).toEqual('/team/22/link/simple'); + expect(nativeLink.className).toEqual(''); + expect(nativeButton.className).toEqual(''); + }), + )); it('should not set the class until the first navigation succeeds', fakeAsync(() => { - @Component({ - template: - '' - }) - class RootCmpWithLink { - } + @Component({ + template: + '', + }) + class RootCmpWithLink {} - TestBed.configureTestingModule({declarations: [RootCmpWithLink]}); - const router: Router = TestBed.inject(Router); + TestBed.configureTestingModule({declarations: [RootCmpWithLink]}); + const router: Router = TestBed.inject(Router); - const f = TestBed.createComponent(RootCmpWithLink); - advance(f); + const f = TestBed.createComponent(RootCmpWithLink); + advance(f); - const link = f.nativeElement.querySelector('a'); - expect(link.className).toEqual(''); + const link = f.nativeElement.querySelector('a'); + expect(link.className).toEqual(''); - router.initialNavigation(); - advance(f); + router.initialNavigation(); + advance(f); - expect(link.className).toEqual('active'); - })); + expect(link.className).toEqual('active'); + })); + it('should set the class on a parent element when the link is active', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - it('should set the class on a parent element when the link is active', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + { + path: 'link', + component: DummyLinkWithParentCmp, + children: [ + {path: 'simple', component: SimpleCmp}, + {path: '', component: BlankCmp}, + ], + }, + ], + }, + ]); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [{ - path: 'link', - component: DummyLinkWithParentCmp, - children: [{path: 'simple', component: SimpleCmp}, {path: '', component: BlankCmp}] - }] - }]); + router.navigateByUrl('/team/22/link;exact=true'); + advance(fixture); + advance(fixture); + expect(location.path()).toEqual('/team/22/link;exact=true'); - router.navigateByUrl('/team/22/link;exact=true'); - advance(fixture); - advance(fixture); - expect(location.path()).toEqual('/team/22/link;exact=true'); + const native = fixture.nativeElement.querySelector('#link-parent'); + expect(native.className).toEqual('active'); - const native = fixture.nativeElement.querySelector('#link-parent'); - expect(native.className).toEqual('active'); + router.navigateByUrl('/team/22/link/simple'); + advance(fixture); + expect(location.path()).toEqual('/team/22/link/simple'); + expect(native.className).toEqual(''); + }), + )); - router.navigateByUrl('/team/22/link/simple'); - advance(fixture); - expect(location.path()).toEqual('/team/22/link/simple'); - expect(native.className).toEqual(''); - }))); + it('should set the class when the link is active', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - it('should set the class when the link is active', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + { + path: 'link', + component: DummyLinkCmp, + children: [ + {path: 'simple', component: SimpleCmp}, + {path: '', component: BlankCmp}, + ], + }, + ], + }, + ]); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [{ - path: 'link', - component: DummyLinkCmp, - children: [{path: 'simple', component: SimpleCmp}, {path: '', component: BlankCmp}] - }] - }]); + router.navigateByUrl('/team/22/link'); + advance(fixture); + advance(fixture); + expect(location.path()).toEqual('/team/22/link'); - router.navigateByUrl('/team/22/link'); - advance(fixture); - advance(fixture); - expect(location.path()).toEqual('/team/22/link'); + const native = fixture.nativeElement.querySelector('a'); + expect(native.className).toEqual('active'); - const native = fixture.nativeElement.querySelector('a'); - expect(native.className).toEqual('active'); - - router.navigateByUrl('/team/22/link/simple'); - advance(fixture); - expect(location.path()).toEqual('/team/22/link/simple'); - expect(native.className).toEqual('active'); - }))); + router.navigateByUrl('/team/22/link/simple'); + advance(fixture); + expect(location.path()).toEqual('/team/22/link/simple'); + expect(native.className).toEqual('active'); + }), + )); it('should expose an isActive property', fakeAsync(() => { - @Component({ - template: ` + @Component({ + template: `

{{rla.isActive}}

- ` - }) - class ComponentWithRouterLink { - } + `, + }) + class ComponentWithRouterLink {} - TestBed.configureTestingModule({declarations: [ComponentWithRouterLink]}); - const router: Router = TestBed.inject(Router); + TestBed.configureTestingModule({declarations: [ComponentWithRouterLink]}); + const router: Router = TestBed.inject(Router); - router.resetConfig([ - { - path: 'team', - component: TeamCmp, - }, - { - path: 'otherteam', - component: TeamCmp, - } - ]); + router.resetConfig([ + { + path: 'team', + component: TeamCmp, + }, + { + path: 'otherteam', + component: TeamCmp, + }, + ]); - const fixture = TestBed.createComponent(ComponentWithRouterLink); - router.navigateByUrl('/team'); - expect(() => advance(fixture)).not.toThrow(); - advance(fixture); + const fixture = TestBed.createComponent(ComponentWithRouterLink); + router.navigateByUrl('/team'); + expect(() => advance(fixture)).not.toThrow(); + advance(fixture); - const paragraph = fixture.nativeElement.querySelector('p'); - expect(paragraph.textContent).toEqual('true'); + const paragraph = fixture.nativeElement.querySelector('p'); + expect(paragraph.textContent).toEqual('true'); - router.navigateByUrl('/otherteam'); - advance(fixture); - advance(fixture); - expect(paragraph.textContent).toEqual('false'); - })); + router.navigateByUrl('/otherteam'); + advance(fixture); + advance(fixture); + expect(paragraph.textContent).toEqual('false'); + })); it('should not trigger change detection when active state has not changed', fakeAsync(() => { - @Component({ - template: ``, - }) - class LinkComponent { - link = 'notactive'; - } + @Component({ + template: ``, + }) + class LinkComponent { + link = 'notactive'; + } - @Component({template: ''}) - class SimpleComponent { - } + @Component({template: ''}) + class SimpleComponent {} - TestBed.configureTestingModule({ - imports: [ - ...ROUTER_DIRECTIVES, - ], - providers: [ - provideRouter([{path: '', component: SimpleComponent}]), - ], - declarations: [LinkComponent, SimpleComponent] - }); + TestBed.configureTestingModule({ + imports: [...ROUTER_DIRECTIVES], + providers: [provideRouter([{path: '', component: SimpleComponent}])], + declarations: [LinkComponent, SimpleComponent], + }); - const fixture = createRoot(TestBed.inject(Router), LinkComponent); - fixture.componentInstance.link = 'stillnotactive'; - fixture.detectChanges(false /** checkNoChanges */); - expect(TestBed.inject(NgZone).hasPendingMicrotasks).toBe(false); - })); + const fixture = createRoot(TestBed.inject(Router), LinkComponent); + fixture.componentInstance.link = 'stillnotactive'; + fixture.detectChanges(false /** checkNoChanges */); + expect(TestBed.inject(NgZone).hasPendingMicrotasks).toBe(false); + })); - it('should emit on isActiveChange output when link is activated or inactivated', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should emit on isActiveChange output when link is activated or inactivated', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [{ - path: 'link', - component: DummyLinkCmp, - children: [{path: 'simple', component: SimpleCmp}, {path: '', component: BlankCmp}] - }] - }]); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + { + path: 'link', + component: DummyLinkCmp, + children: [ + {path: 'simple', component: SimpleCmp}, + {path: '', component: BlankCmp}, + ], + }, + ], + }, + ]); - router.navigateByUrl('/team/22/link;exact=true'); - advance(fixture); - advance(fixture); - expect(location.path()).toEqual('/team/22/link;exact=true'); + router.navigateByUrl('/team/22/link;exact=true'); + advance(fixture); + advance(fixture); + expect(location.path()).toEqual('/team/22/link;exact=true'); - const linkComponent = - fixture.debugElement.query(By.directive(DummyLinkCmp)).componentInstance as - DummyLinkCmp; + const linkComponent = fixture.debugElement.query(By.directive(DummyLinkCmp)) + .componentInstance as DummyLinkCmp; - expect(linkComponent.isLinkActivated).toEqual(true); - const nativeLink = fixture.nativeElement.querySelector('a'); - const nativeButton = fixture.nativeElement.querySelector('button'); - expect(nativeLink.className).toEqual('active'); - expect(nativeButton.className).toEqual('active'); + expect(linkComponent.isLinkActivated).toEqual(true); + const nativeLink = fixture.nativeElement.querySelector('a'); + const nativeButton = fixture.nativeElement.querySelector('button'); + expect(nativeLink.className).toEqual('active'); + expect(nativeButton.className).toEqual('active'); + router.navigateByUrl('/team/22/link/simple'); + advance(fixture); + expect(location.path()).toEqual('/team/22/link/simple'); + expect(linkComponent.isLinkActivated).toEqual(false); + expect(nativeLink.className).toEqual(''); + expect(nativeButton.className).toEqual(''); + }), + )); - router.navigateByUrl('/team/22/link/simple'); - advance(fixture); - expect(location.path()).toEqual('/team/22/link/simple'); - expect(linkComponent.isLinkActivated).toEqual(false); - expect(nativeLink.className).toEqual(''); - expect(nativeButton.className).toEqual(''); - }))); + it('should set a provided aria-current attribute when the link is active (a tag)', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - it('should set a provided aria-current attribute when the link is active (a tag)', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + { + path: 'link', + component: DummyLinkCmp, + children: [ + {path: 'simple', component: SimpleCmp}, + {path: '', component: BlankCmp}, + ], + }, + ], + }, + ]); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [{ - path: 'link', - component: DummyLinkCmp, - children: [{path: 'simple', component: SimpleCmp}, {path: '', component: BlankCmp}] - }] - }]); + router.navigateByUrl('/team/22/link;exact=true'); + advance(fixture); + advance(fixture); + expect(location.path()).toEqual('/team/22/link;exact=true'); - router.navigateByUrl('/team/22/link;exact=true'); - advance(fixture); - advance(fixture); - expect(location.path()).toEqual('/team/22/link;exact=true'); + const nativeLink = fixture.nativeElement.querySelector('a'); + const nativeButton = fixture.nativeElement.querySelector('button'); + expect(nativeLink.getAttribute('aria-current')).toEqual('page'); + expect(nativeButton.hasAttribute('aria-current')).toEqual(false); - const nativeLink = fixture.nativeElement.querySelector('a'); - const nativeButton = fixture.nativeElement.querySelector('button'); - expect(nativeLink.getAttribute('aria-current')).toEqual('page'); - expect(nativeButton.hasAttribute('aria-current')).toEqual(false); - - router.navigateByUrl('/team/22/link/simple'); - advance(fixture); - expect(location.path()).toEqual('/team/22/link/simple'); - expect(nativeLink.hasAttribute('aria-current')).toEqual(false); - expect(nativeButton.hasAttribute('aria-current')).toEqual(false); - }))); + router.navigateByUrl('/team/22/link/simple'); + advance(fixture); + expect(location.path()).toEqual('/team/22/link/simple'); + expect(nativeLink.hasAttribute('aria-current')).toEqual(false); + expect(nativeButton.hasAttribute('aria-current')).toEqual(false); + }), + )); }); describe('lazy loading', () => { - it('works', fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - @Component( - {selector: 'lazy', template: 'lazy-loaded-parent []'}) - class ParentLazyLoadedComponent { - } + it('works', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + @Component({ + selector: 'lazy', + template: 'lazy-loaded-parent []', + }) + class ParentLazyLoadedComponent {} - @Component({selector: 'lazy', template: 'lazy-loaded-child'}) - class ChildLazyLoadedComponent { - } + @Component({selector: 'lazy', template: 'lazy-loaded-child'}) + class ChildLazyLoadedComponent {} - @NgModule({ - declarations: [ParentLazyLoadedComponent, ChildLazyLoadedComponent], - imports: [RouterModule.forChild([{ - path: 'loaded', - component: ParentLazyLoadedComponent, - children: [{path: 'child', component: ChildLazyLoadedComponent}] - }])] - }) - class LoadedModule { - } + @NgModule({ + declarations: [ParentLazyLoadedComponent, ChildLazyLoadedComponent], + imports: [ + RouterModule.forChild([ + { + path: 'loaded', + component: ParentLazyLoadedComponent, + children: [{path: 'child', component: ChildLazyLoadedComponent}], + }, + ]), + ], + }) + class LoadedModule {} - const fixture = createRoot(router, RootCmp); + const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); + router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); - router.navigateByUrl('/lazy/loaded/child'); - advance(fixture); + router.navigateByUrl('/lazy/loaded/child'); + advance(fixture); - expect(location.path()).toEqual('/lazy/loaded/child'); - expect(fixture.nativeElement).toHaveText('lazy-loaded-parent [lazy-loaded-child]'); - }))); + expect(location.path()).toEqual('/lazy/loaded/child'); + expect(fixture.nativeElement).toHaveText('lazy-loaded-parent [lazy-loaded-child]'); + }), + )); - it('should have 2 injector trees: module and element', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - @Component({ - selector: 'lazy', - template: 'parent[]', - viewProviders: [ - {provide: 'shadow', useValue: 'from parent component'}, - ], - }) - class Parent { - } + it('should have 2 injector trees: module and element', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + @Component({ + selector: 'lazy', + template: 'parent[]', + viewProviders: [{provide: 'shadow', useValue: 'from parent component'}], + }) + class Parent {} - @Component({selector: 'lazy', template: 'child'}) - class Child { - } + @Component({selector: 'lazy', template: 'child'}) + class Child {} - @NgModule({ - declarations: [Parent], - imports: [RouterModule.forChild([{ - path: 'parent', - component: Parent, - children: [ - {path: 'child', loadChildren: () => ChildModule}, - ] - }])], - providers: [ - {provide: 'moduleName', useValue: 'parent'}, - {provide: 'fromParent', useValue: 'from parent'}, - ], - }) - class ParentModule { - } + @NgModule({ + declarations: [Parent], + imports: [ + RouterModule.forChild([ + { + path: 'parent', + component: Parent, + children: [{path: 'child', loadChildren: () => ChildModule}], + }, + ]), + ], + providers: [ + {provide: 'moduleName', useValue: 'parent'}, + {provide: 'fromParent', useValue: 'from parent'}, + ], + }) + class ParentModule {} - @NgModule({ - declarations: [Child], - imports: [RouterModule.forChild([{path: '', component: Child}])], - providers: [ - {provide: 'moduleName', useValue: 'child'}, - {provide: 'fromChild', useValue: 'from child'}, - {provide: 'shadow', useValue: 'from child module'}, - ], - }) - class ChildModule { - } + @NgModule({ + declarations: [Child], + imports: [RouterModule.forChild([{path: '', component: Child}])], + providers: [ + {provide: 'moduleName', useValue: 'child'}, + {provide: 'fromChild', useValue: 'from child'}, + {provide: 'shadow', useValue: 'from child module'}, + ], + }) + class ChildModule {} - const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'lazy', loadChildren: () => ParentModule}]); - router.navigateByUrl('/lazy/parent/child'); - advance(fixture); - expect(location.path()).toEqual('/lazy/parent/child'); - expect(fixture.nativeElement).toHaveText('parent[child]'); + const fixture = createRoot(router, RootCmp); + router.resetConfig([{path: 'lazy', loadChildren: () => ParentModule}]); + router.navigateByUrl('/lazy/parent/child'); + advance(fixture); + expect(location.path()).toEqual('/lazy/parent/child'); + expect(fixture.nativeElement).toHaveText('parent[child]'); - const pInj = fixture.debugElement.query(By.directive(Parent)).injector!; - const cInj = fixture.debugElement.query(By.directive(Child)).injector!; + const pInj = fixture.debugElement.query(By.directive(Parent)).injector!; + const cInj = fixture.debugElement.query(By.directive(Child)).injector!; - expect(pInj.get('moduleName')).toEqual('parent'); - expect(pInj.get('fromParent')).toEqual('from parent'); - expect(pInj.get(Parent)).toBeInstanceOf(Parent); - expect(pInj.get('fromChild', null)).toEqual(null); - expect(pInj.get(Child, null)).toEqual(null); + expect(pInj.get('moduleName')).toEqual('parent'); + expect(pInj.get('fromParent')).toEqual('from parent'); + expect(pInj.get(Parent)).toBeInstanceOf(Parent); + expect(pInj.get('fromChild', null)).toEqual(null); + expect(pInj.get(Child, null)).toEqual(null); - expect(cInj.get('moduleName')).toEqual('child'); - expect(cInj.get('fromParent')).toEqual('from parent'); - expect(cInj.get('fromChild')).toEqual('from child'); - expect(cInj.get(Parent)).toBeInstanceOf(Parent); - expect(cInj.get(Child)).toBeInstanceOf(Child); - // The child module can not shadow the parent component - expect(cInj.get('shadow')).toEqual('from parent component'); + expect(cInj.get('moduleName')).toEqual('child'); + expect(cInj.get('fromParent')).toEqual('from parent'); + expect(cInj.get('fromChild')).toEqual('from child'); + expect(cInj.get(Parent)).toBeInstanceOf(Parent); + expect(cInj.get(Child)).toBeInstanceOf(Child); + // The child module can not shadow the parent component + expect(cInj.get('shadow')).toEqual('from parent component'); - const pmInj = pInj.get(NgModuleRef).injector; - const cmInj = cInj.get(NgModuleRef).injector; + const pmInj = pInj.get(NgModuleRef).injector; + const cmInj = cInj.get(NgModuleRef).injector; - expect(pmInj.get('moduleName')).toEqual('parent'); - expect(cmInj.get('moduleName')).toEqual('child'); + expect(pmInj.get('moduleName')).toEqual('parent'); + expect(cmInj.get('moduleName')).toEqual('child'); - expect(pmInj.get(Parent, '-')).toEqual('-'); - expect(cmInj.get(Parent, '-')).toEqual('-'); - expect(pmInj.get(Child, '-')).toEqual('-'); - expect(cmInj.get(Child, '-')).toEqual('-'); - }))); + expect(pmInj.get(Parent, '-')).toEqual('-'); + expect(cmInj.get(Parent, '-')).toEqual('-'); + expect(pmInj.get(Child, '-')).toEqual('-'); + expect(cmInj.get(Child, '-')).toEqual('-'); + }), + )); // https://github.com/angular/angular/issues/12889 - it('should create a single instance of lazy-loaded modules', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - @Component( - {selector: 'lazy', template: 'lazy-loaded-parent []'}) - class ParentLazyLoadedComponent { - } + it('should create a single instance of lazy-loaded modules', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + @Component({ + selector: 'lazy', + template: 'lazy-loaded-parent []', + }) + class ParentLazyLoadedComponent {} - @Component({selector: 'lazy', template: 'lazy-loaded-child'}) - class ChildLazyLoadedComponent { - } + @Component({selector: 'lazy', template: 'lazy-loaded-child'}) + class ChildLazyLoadedComponent {} - @NgModule({ - declarations: [ParentLazyLoadedComponent, ChildLazyLoadedComponent], - imports: [RouterModule.forChild([{ - path: 'loaded', - component: ParentLazyLoadedComponent, - children: [{path: 'child', component: ChildLazyLoadedComponent}] - }])] - }) - class LoadedModule { - static instances = 0; - constructor() { - LoadedModule.instances++; - } - } + @NgModule({ + declarations: [ParentLazyLoadedComponent, ChildLazyLoadedComponent], + imports: [ + RouterModule.forChild([ + { + path: 'loaded', + component: ParentLazyLoadedComponent, + children: [{path: 'child', component: ChildLazyLoadedComponent}], + }, + ]), + ], + }) + class LoadedModule { + static instances = 0; + constructor() { + LoadedModule.instances++; + } + } - const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); - router.navigateByUrl('/lazy/loaded/child'); - advance(fixture); - expect(fixture.nativeElement).toHaveText('lazy-loaded-parent [lazy-loaded-child]'); - expect(LoadedModule.instances).toEqual(1); - }))); + const fixture = createRoot(router, RootCmp); + router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); + router.navigateByUrl('/lazy/loaded/child'); + advance(fixture); + expect(fixture.nativeElement).toHaveText('lazy-loaded-parent [lazy-loaded-child]'); + expect(LoadedModule.instances).toEqual(1); + }), + )); // https://github.com/angular/angular/issues/13870 - it('should create a single instance of guards for lazy-loaded modules', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - @Injectable() - class Service { - } + it('should create a single instance of guards for lazy-loaded modules', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + @Injectable() + class Service {} - @Injectable() - class Resolver { - constructor(public service: Service) {} - resolve() { - return this.service; - } - } + @Injectable() + class Resolver { + constructor(public service: Service) {} + resolve() { + return this.service; + } + } - @Component({selector: 'lazy', template: 'lazy'}) - class LazyLoadedComponent { - resolvedService: Service; - constructor(public injectedService: Service, route: ActivatedRoute) { - this.resolvedService = route.snapshot.data['service']; - } - } + @Component({selector: 'lazy', template: 'lazy'}) + class LazyLoadedComponent { + resolvedService: Service; + constructor( + public injectedService: Service, + route: ActivatedRoute, + ) { + this.resolvedService = route.snapshot.data['service']; + } + } - @NgModule({ - declarations: [LazyLoadedComponent], - providers: [Service, Resolver], - imports: [ - RouterModule.forChild([{ - path: 'loaded', - component: LazyLoadedComponent, - resolve: {'service': () => coreInject(Resolver).resolve()}, - }]), - ] - }) - class LoadedModule { - } + @NgModule({ + declarations: [LazyLoadedComponent], + providers: [Service, Resolver], + imports: [ + RouterModule.forChild([ + { + path: 'loaded', + component: LazyLoadedComponent, + resolve: {'service': () => coreInject(Resolver).resolve()}, + }, + ]), + ], + }) + class LoadedModule {} - const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); - router.navigateByUrl('/lazy/loaded'); - advance(fixture); + const fixture = createRoot(router, RootCmp); + router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); + router.navigateByUrl('/lazy/loaded'); + advance(fixture); - expect(fixture.nativeElement).toHaveText('lazy'); - const lzc = - fixture.debugElement.query(By.directive(LazyLoadedComponent)).componentInstance; - expect(lzc.injectedService).toBe(lzc.resolvedService); - }))); + expect(fixture.nativeElement).toHaveText('lazy'); + const lzc = fixture.debugElement.query( + By.directive(LazyLoadedComponent), + ).componentInstance; + expect(lzc.injectedService).toBe(lzc.resolvedService); + }), + )); + it('should emit RouteConfigLoadStart and RouteConfigLoadEnd event when route is lazy loaded', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + @Component({ + selector: 'lazy', + template: 'lazy-loaded-parent []', + }) + class ParentLazyLoadedComponent {} - it('should emit RouteConfigLoadStart and RouteConfigLoadEnd event when route is lazy loaded', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - @Component({ - selector: 'lazy', - template: 'lazy-loaded-parent []', - }) - class ParentLazyLoadedComponent { - } + @Component({selector: 'lazy', template: 'lazy-loaded-child'}) + class ChildLazyLoadedComponent {} - @Component({selector: 'lazy', template: 'lazy-loaded-child'}) - class ChildLazyLoadedComponent { - } + @NgModule({ + declarations: [ParentLazyLoadedComponent, ChildLazyLoadedComponent], + imports: [ + RouterModule.forChild([ + { + path: 'loaded', + component: ParentLazyLoadedComponent, + children: [{path: 'child', component: ChildLazyLoadedComponent}], + }, + ]), + ], + }) + class LoadedModule {} - @NgModule({ - declarations: [ParentLazyLoadedComponent, ChildLazyLoadedComponent], - imports: [RouterModule.forChild([{ - path: 'loaded', - component: ParentLazyLoadedComponent, - children: [{path: 'child', component: ChildLazyLoadedComponent}], - }])] - }) - class LoadedModule { - } + const events: Array = []; - const events: Array = []; + router.events.subscribe((e) => { + if (e instanceof RouteConfigLoadStart || e instanceof RouteConfigLoadEnd) { + events.push(e); + } + }); - router.events.subscribe(e => { - if (e instanceof RouteConfigLoadStart || e instanceof RouteConfigLoadEnd) { - events.push(e); - } - }); + const fixture = createRoot(router, RootCmp); + router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); - const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); + router.navigateByUrl('/lazy/loaded/child'); + advance(fixture); - router.navigateByUrl('/lazy/loaded/child'); - advance(fixture); + expect(events.length).toEqual(2); + expect(events[0].toString()).toEqual('RouteConfigLoadStart(path: lazy)'); + expect(events[1].toString()).toEqual('RouteConfigLoadEnd(path: lazy)'); + }), + )); - expect(events.length).toEqual(2); - expect(events[0].toString()).toEqual('RouteConfigLoadStart(path: lazy)'); - expect(events[1].toString()).toEqual('RouteConfigLoadEnd(path: lazy)'); - }))); + it('throws an error when forRoot() is used in a lazy context', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + @Component({selector: 'lazy', template: 'should not show'}) + class LazyLoadedComponent {} - it('throws an error when forRoot() is used in a lazy context', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - @Component({selector: 'lazy', template: 'should not show'}) - class LazyLoadedComponent { - } + @NgModule({ + declarations: [LazyLoadedComponent], + imports: [RouterModule.forRoot([{path: 'loaded', component: LazyLoadedComponent}])], + }) + class LoadedModule {} - @NgModule({ - declarations: [LazyLoadedComponent], - imports: [RouterModule.forRoot([{path: 'loaded', component: LazyLoadedComponent}])] - }) - class LoadedModule { - } + const fixture = createRoot(router, RootCmp); - const fixture = createRoot(router, RootCmp); + router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); - router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); + let recordedError: any = null; + router.navigateByUrl('/lazy/loaded')!.catch((err) => (recordedError = err)); + advance(fixture); + expect(recordedError.message).toContain(`NG04007`); + }), + )); - let recordedError: any = null; - router.navigateByUrl('/lazy/loaded')!.catch(err => recordedError = err); - advance(fixture); - expect(recordedError.message).toContain(`NG04007`); - }))); + it('should combine routes from multiple modules into a single configuration', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + @Component({selector: 'lazy', template: 'lazy-loaded-2'}) + class LazyComponent2 {} - it('should combine routes from multiple modules into a single configuration', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - @Component({selector: 'lazy', template: 'lazy-loaded-2'}) - class LazyComponent2 { - } + @NgModule({ + declarations: [LazyComponent2], + imports: [RouterModule.forChild([{path: 'loaded', component: LazyComponent2}])], + }) + class SiblingOfLoadedModule {} - @NgModule({ - declarations: [LazyComponent2], - imports: [RouterModule.forChild([{path: 'loaded', component: LazyComponent2}])] - }) - class SiblingOfLoadedModule { - } + @Component({selector: 'lazy', template: 'lazy-loaded-1'}) + class LazyComponent1 {} - @Component({selector: 'lazy', template: 'lazy-loaded-1'}) - class LazyComponent1 { - } + @NgModule({ + declarations: [LazyComponent1], + imports: [ + RouterModule.forChild([{path: 'loaded', component: LazyComponent1}]), + SiblingOfLoadedModule, + ], + }) + class LoadedModule {} - @NgModule({ - declarations: [LazyComponent1], - imports: [ - RouterModule.forChild([{path: 'loaded', component: LazyComponent1}]), - SiblingOfLoadedModule - ] - }) - class LoadedModule { - } + const fixture = createRoot(router, RootCmp); - const fixture = createRoot(router, RootCmp); + router.resetConfig([ + {path: 'lazy1', loadChildren: () => LoadedModule}, + {path: 'lazy2', loadChildren: () => SiblingOfLoadedModule}, + ]); - router.resetConfig([ - {path: 'lazy1', loadChildren: () => LoadedModule}, - {path: 'lazy2', loadChildren: () => SiblingOfLoadedModule} - ]); + router.navigateByUrl('/lazy1/loaded'); + advance(fixture); + expect(location.path()).toEqual('/lazy1/loaded'); - router.navigateByUrl('/lazy1/loaded'); - advance(fixture); - expect(location.path()).toEqual('/lazy1/loaded'); + router.navigateByUrl('/lazy2/loaded'); + advance(fixture); + expect(location.path()).toEqual('/lazy2/loaded'); + }), + )); - router.navigateByUrl('/lazy2/loaded'); - advance(fixture); - expect(location.path()).toEqual('/lazy2/loaded'); - }))); + it('should allow lazy loaded module in named outlet', fakeAsync( + inject([Router], (router: Router) => { + @Component({selector: 'lazy', template: 'lazy-loaded'}) + class LazyComponent {} - it('should allow lazy loaded module in named outlet', - fakeAsync(inject([Router], (router: Router) => { - @Component({selector: 'lazy', template: 'lazy-loaded'}) - class LazyComponent { - } + @NgModule({ + declarations: [LazyComponent], + imports: [RouterModule.forChild([{path: '', component: LazyComponent}])], + }) + class LazyLoadedModule {} - @NgModule({ - declarations: [LazyComponent], - imports: [RouterModule.forChild([{path: '', component: LazyComponent}])] - }) - class LazyLoadedModule { - } + const fixture = createRoot(router, RootCmp); - const fixture = createRoot(router, RootCmp); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'user/:name', component: UserCmp}, + {path: 'lazy', loadChildren: () => LazyLoadedModule, outlet: 'right'}, + ], + }, + ]); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'user/:name', component: UserCmp}, - {path: 'lazy', loadChildren: () => LazyLoadedModule, outlet: 'right'}, - ] - }]); + router.navigateByUrl('/team/22/user/john'); + advance(fixture); + expect(fixture.nativeElement).toHaveText('team 22 [ user john, right: ]'); - router.navigateByUrl('/team/22/user/john'); - advance(fixture); + router.navigateByUrl('/team/22/(user/john//right:lazy)'); + advance(fixture); - expect(fixture.nativeElement).toHaveText('team 22 [ user john, right: ]'); + expect(fixture.nativeElement).toHaveText('team 22 [ user john, right: lazy-loaded ]'); + }), + )); - router.navigateByUrl('/team/22/(user/john//right:lazy)'); - advance(fixture); + it('should allow componentless named outlet to render children', fakeAsync( + inject([Router], (router: Router) => { + const fixture = createRoot(router, RootCmp); - expect(fixture.nativeElement).toHaveText('team 22 [ user john, right: lazy-loaded ]'); - }))); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'user/:name', component: UserCmp}, + {path: 'simple', outlet: 'right', children: [{path: '', component: SimpleCmp}]}, + ], + }, + ]); - it('should allow componentless named outlet to render children', - fakeAsync(inject([Router], (router: Router) => { - const fixture = createRoot(router, RootCmp); + router.navigateByUrl('/team/22/user/john'); + advance(fixture); - router.resetConfig([{ - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'user/:name', component: UserCmp}, - {path: 'simple', outlet: 'right', children: [{path: '', component: SimpleCmp}]}, - ] - }]); + expect(fixture.nativeElement).toHaveText('team 22 [ user john, right: ]'); + router.navigateByUrl('/team/22/(user/john//right:simple)'); + advance(fixture); - router.navigateByUrl('/team/22/user/john'); - advance(fixture); - - expect(fixture.nativeElement).toHaveText('team 22 [ user john, right: ]'); - - router.navigateByUrl('/team/22/(user/john//right:simple)'); - advance(fixture); - - expect(fixture.nativeElement).toHaveText('team 22 [ user john, right: simple ]'); - }))); + expect(fixture.nativeElement).toHaveText('team 22 [ user john, right: simple ]'); + }), + )); it('should render loadComponent named outlet with children', fakeAsync(() => { - const router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmp); + const router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmp); - @Component({ - standalone: true, - imports: [RouterModule], - template: '[right outlet component: ]', - }) - class RightComponent { - constructor(readonly route: ActivatedRoute) {} - } + @Component({ + standalone: true, + imports: [RouterModule], + template: '[right outlet component: ]', + }) + class RightComponent { + constructor(readonly route: ActivatedRoute) {} + } - const loadSpy = jasmine.createSpy(); - loadSpy.and.returnValue(RightComponent); + const loadSpy = jasmine.createSpy(); + loadSpy.and.returnValue(RightComponent); - router.resetConfig([ - { - path: 'team/:id', - component: TeamCmp, - children: [ - {path: 'user/:name', component: UserCmp}, - { - path: 'simple', - loadComponent: loadSpy, - outlet: 'right', - children: [{path: '', component: SimpleCmp}] - }, - ] - }, - {path: '', component: SimpleCmp} - ]); + router.resetConfig([ + { + path: 'team/:id', + component: TeamCmp, + children: [ + {path: 'user/:name', component: UserCmp}, + { + path: 'simple', + loadComponent: loadSpy, + outlet: 'right', + children: [{path: '', component: SimpleCmp}], + }, + ], + }, + {path: '', component: SimpleCmp}, + ]); - router.navigateByUrl('/team/22/(user/john//right:simple)'); - advance(fixture); + router.navigateByUrl('/team/22/(user/john//right:simple)'); + advance(fixture); - expect(fixture.nativeElement) - .toHaveText('team 22 [ user john, right: [right outlet component: simple] ]'); - const rightCmp: RightComponent = - fixture.debugElement.query(By.directive(RightComponent)).componentInstance; - // Ensure we don't accidentally add `EmptyOutletComponent` via `standardizeConfig` - expect(rightCmp.route.routeConfig?.component).not.toBeDefined(); + expect(fixture.nativeElement).toHaveText( + 'team 22 [ user john, right: [right outlet component: simple] ]', + ); + const rightCmp: RightComponent = fixture.debugElement.query( + By.directive(RightComponent), + ).componentInstance; + // Ensure we don't accidentally add `EmptyOutletComponent` via `standardizeConfig` + expect(rightCmp.route.routeConfig?.component).not.toBeDefined(); - // Ensure we can navigate away and come back - router.navigateByUrl('/'); - advance(fixture); - router.navigateByUrl('/team/22/(user/john//right:simple)'); - advance(fixture); - expect(fixture.nativeElement) - .toHaveText('team 22 [ user john, right: [right outlet component: simple] ]'); - expect(loadSpy.calls.count()).toEqual(1); - })); + // Ensure we can navigate away and come back + router.navigateByUrl('/'); + advance(fixture); + router.navigateByUrl('/team/22/(user/john//right:simple)'); + advance(fixture); + expect(fixture.nativeElement).toHaveText( + 'team 22 [ user john, right: [right outlet component: simple] ]', + ); + expect(loadSpy.calls.count()).toEqual(1); + })); describe('should use the injector of the lazily-loaded configuration', () => { class LazyLoadedServiceDefinedInModule {} @@ -6177,15 +6671,13 @@ for (const browserAPI of ['navigation', 'history'] as const) { selector: 'eager-parent', template: 'eager-parent ', }) - class EagerParentComponent { - } + class EagerParentComponent {} @Component({ selector: 'lazy-parent', template: 'lazy-parent ', }) - class LazyParentComponent { - } + class LazyParentComponent {} @Component({ selector: 'lazy-child', @@ -6193,32 +6685,35 @@ for (const browserAPI of ['navigation', 'history'] as const) { }) class LazyChildComponent { constructor( - lazy: LazyParentComponent, // should be able to inject lazy/direct parent - lazyService: - LazyLoadedServiceDefinedInModule, // should be able to inject lazy service - eager: EagerParentComponent // should use the injector of the location to create a - // parent + lazy: LazyParentComponent, // should be able to inject lazy/direct parent + lazyService: LazyLoadedServiceDefinedInModule, // should be able to inject lazy service + eager: EagerParentComponent, // should use the injector of the location to create a + // parent ) {} } @NgModule({ declarations: [LazyParentComponent, LazyChildComponent], - imports: [RouterModule.forChild([{ - path: '', - children: [{ - path: 'lazy-parent', - component: LazyParentComponent, - children: [{path: 'lazy-child', component: LazyChildComponent}] - }] - }])], - providers: [LazyLoadedServiceDefinedInModule] + imports: [ + RouterModule.forChild([ + { + path: '', + children: [ + { + path: 'lazy-parent', + component: LazyParentComponent, + children: [{path: 'lazy-child', component: LazyChildComponent}], + }, + ], + }, + ]), + ], + providers: [LazyLoadedServiceDefinedInModule], }) - class LoadedModule { - } + class LoadedModule {} @NgModule({declarations: [EagerParentComponent], imports: [RouterModule.forRoot([])]}) - class TestModule { - } + class TestModule {} beforeEach(() => { TestBed.configureTestingModule({ @@ -6226,235 +6721,236 @@ for (const browserAPI of ['navigation', 'history'] as const) { }); }); - it('should use the injector of the lazily-loaded configuration', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should use the injector of the lazily-loaded configuration', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'eager-parent', - component: EagerParentComponent, - children: [{path: 'lazy', loadChildren: () => LoadedModule}] - }]); + router.resetConfig([ + { + path: 'eager-parent', + component: EagerParentComponent, + children: [{path: 'lazy', loadChildren: () => LoadedModule}], + }, + ]); - router.navigateByUrl('/eager-parent/lazy/lazy-parent/lazy-child'); - advance(fixture); + router.navigateByUrl('/eager-parent/lazy/lazy-parent/lazy-child'); + advance(fixture); - expect(location.path()).toEqual('/eager-parent/lazy/lazy-parent/lazy-child'); - expect(fixture.nativeElement).toHaveText('eager-parent lazy-parent lazy-child'); - }))); + expect(location.path()).toEqual('/eager-parent/lazy/lazy-parent/lazy-child'); + expect(fixture.nativeElement).toHaveText('eager-parent lazy-parent lazy-child'); + }), + )); }); - it('works when given a callback', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - @Component({selector: 'lazy', template: 'lazy-loaded'}) - class LazyLoadedComponent { - } + it('works when given a callback', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + @Component({selector: 'lazy', template: 'lazy-loaded'}) + class LazyLoadedComponent {} - @NgModule({ - declarations: [LazyLoadedComponent], - imports: [RouterModule.forChild([{path: 'loaded', component: LazyLoadedComponent}])], - }) - class LoadedModule { - } + @NgModule({ + declarations: [LazyLoadedComponent], + imports: [RouterModule.forChild([{path: 'loaded', component: LazyLoadedComponent}])], + }) + class LoadedModule {} - const fixture = createRoot(router, RootCmp); + const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); + router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); - router.navigateByUrl('/lazy/loaded'); - advance(fixture); + router.navigateByUrl('/lazy/loaded'); + advance(fixture); - expect(location.path()).toEqual('/lazy/loaded'); - expect(fixture.nativeElement).toHaveText('lazy-loaded'); - }))); + expect(location.path()).toEqual('/lazy/loaded'); + expect(fixture.nativeElement).toHaveText('lazy-loaded'); + }), + )); - it('error emit an error when cannot load a config', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('error emit an error when cannot load a config', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'lazy', - loadChildren: () => { - throw new Error('invalid'); - } - }]); + router.resetConfig([ + { + path: 'lazy', + loadChildren: () => { + throw new Error('invalid'); + }, + }, + ]); - const recordedEvents: Event[] = []; - router.events.forEach(e => recordedEvents.push(e)); + const recordedEvents: Event[] = []; + router.events.forEach((e) => recordedEvents.push(e)); - router.navigateByUrl('/lazy/loaded')!.catch(s => {}); - advance(fixture); + router.navigateByUrl('/lazy/loaded')!.catch((s) => {}); + advance(fixture); - expect(location.path()).toEqual(''); + expect(location.path()).toEqual(''); - expectEvents(recordedEvents, [ - [NavigationStart, '/lazy/loaded'], - [RouteConfigLoadStart], - [NavigationError, '/lazy/loaded'], - ]); - }))); + expectEvents(recordedEvents, [ + [NavigationStart, '/lazy/loaded'], + [RouteConfigLoadStart], + [NavigationError, '/lazy/loaded'], + ]); + }), + )); it('should emit an error when the lazily-loaded config is not valid', fakeAsync(() => { - const router = TestBed.inject(Router); - @NgModule({imports: [RouterModule.forChild([{path: 'loaded'}])]}) - class LoadedModule { - } + const router = TestBed.inject(Router); + @NgModule({imports: [RouterModule.forChild([{path: 'loaded'}])]}) + class LoadedModule {} - const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); + const fixture = createRoot(router, RootCmp); + router.resetConfig([{path: 'lazy', loadChildren: () => LoadedModule}]); - let recordedError: any = null; - router.navigateByUrl('/lazy/loaded').catch(err => recordedError = err); - advance(fixture); + let recordedError: any = null; + router.navigateByUrl('/lazy/loaded').catch((err) => (recordedError = err)); + advance(fixture); - expect(recordedError.message) - .toContain( - `Invalid configuration of route 'lazy/loaded'. One of the following must be provided: component, loadComponent, redirectTo, children or loadChildren`); - })); + expect(recordedError.message).toContain( + `Invalid configuration of route 'lazy/loaded'. One of the following must be provided: component, loadComponent, redirectTo, children or loadChildren`, + ); + })); - it('should work with complex redirect rules', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - @Component({selector: 'lazy', template: 'lazy-loaded'}) - class LazyLoadedComponent { - } + it('should work with complex redirect rules', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + @Component({selector: 'lazy', template: 'lazy-loaded'}) + class LazyLoadedComponent {} - @NgModule({ - declarations: [LazyLoadedComponent], - imports: [RouterModule.forChild([{path: 'loaded', component: LazyLoadedComponent}])], - }) - class LoadedModule { - } + @NgModule({ + declarations: [LazyLoadedComponent], + imports: [RouterModule.forChild([{path: 'loaded', component: LazyLoadedComponent}])], + }) + class LoadedModule {} - const fixture = createRoot(router, RootCmp); + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'lazy', loadChildren: () => LoadedModule}, {path: '**', redirectTo: 'lazy'} - ]); + router.resetConfig([ + {path: 'lazy', loadChildren: () => LoadedModule}, + {path: '**', redirectTo: 'lazy'}, + ]); - router.navigateByUrl('/lazy/loaded'); - advance(fixture); + router.navigateByUrl('/lazy/loaded'); + advance(fixture); - expect(location.path()).toEqual('/lazy/loaded'); - }))); + expect(location.path()).toEqual('/lazy/loaded'); + }), + )); - it('should work with wildcard route', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - @Component({selector: 'lazy', template: 'lazy-loaded'}) - class LazyLoadedComponent { - } + it('should work with wildcard route', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + @Component({selector: 'lazy', template: 'lazy-loaded'}) + class LazyLoadedComponent {} - @NgModule({ - declarations: [LazyLoadedComponent], - imports: [RouterModule.forChild([{path: '', component: LazyLoadedComponent}])], - }) - class LazyLoadedModule { - } + @NgModule({ + declarations: [LazyLoadedComponent], + imports: [RouterModule.forChild([{path: '', component: LazyLoadedComponent}])], + }) + class LazyLoadedModule {} - const fixture = createRoot(router, RootCmp); + const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: '**', loadChildren: () => LazyLoadedModule}]); + router.resetConfig([{path: '**', loadChildren: () => LazyLoadedModule}]); - router.navigateByUrl('/lazy'); - advance(fixture); + router.navigateByUrl('/lazy'); + advance(fixture); - expect(location.path()).toEqual('/lazy'); - expect(fixture.nativeElement).toHaveText('lazy-loaded'); - }))); + expect(location.path()).toEqual('/lazy'); + expect(fixture.nativeElement).toHaveText('lazy-loaded'); + }), + )); describe('preloading', () => { let log: string[] = []; @Component({selector: 'lazy', template: 'should not show'}) - class LazyLoadedComponent { - } + class LazyLoadedComponent {} @NgModule({ declarations: [LazyLoadedComponent], - imports: - [RouterModule.forChild([{path: 'LoadedModule2', component: LazyLoadedComponent}])] + imports: [ + RouterModule.forChild([{path: 'LoadedModule2', component: LazyLoadedComponent}]), + ], }) - class LoadedModule2 { - } + class LoadedModule2 {} @NgModule({ - imports: - [RouterModule.forChild([{path: 'LoadedModule1', loadChildren: () => LoadedModule2}])] + imports: [ + RouterModule.forChild([{path: 'LoadedModule1', loadChildren: () => LoadedModule2}]), + ], }) - class LoadedModule1 { - } + class LoadedModule1 {} @NgModule({}) - class EmptyModule { - } + class EmptyModule {} beforeEach(() => { log.length = 0; TestBed.configureTestingModule({ providers: [ - {provide: PreloadingStrategy, useExisting: PreloadAllModules}, { + {provide: PreloadingStrategy, useExisting: PreloadAllModules}, + { provide: 'loggingReturnsTrue', useValue: () => { log.push('loggingReturnsTrue'); return true; - } - } - ] + }, + }, + ], }); const preloader = TestBed.inject(RouterPreloader); preloader.setUpPreloading(); }); it('should work', fakeAsync(() => { - const router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmp); + const router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'blank', component: BlankCmp}, - {path: 'lazy', loadChildren: () => LoadedModule1} - ]); + router.resetConfig([ + {path: 'blank', component: BlankCmp}, + {path: 'lazy', loadChildren: () => LoadedModule1}, + ]); - router.navigateByUrl('/blank'); - advance(fixture); + router.navigateByUrl('/blank'); + advance(fixture); - const config = router.config; - const firstRoutes = getLoadedRoutes(config[1])!; + const config = router.config; + const firstRoutes = getLoadedRoutes(config[1])!; - expect(firstRoutes).toBeDefined(); - expect(firstRoutes[0].path).toEqual('LoadedModule1'); + expect(firstRoutes).toBeDefined(); + expect(firstRoutes[0].path).toEqual('LoadedModule1'); - const secondRoutes = getLoadedRoutes(firstRoutes[0])!; - expect(secondRoutes).toBeDefined(); - expect(secondRoutes[0].path).toEqual('LoadedModule2'); - })); + const secondRoutes = getLoadedRoutes(firstRoutes[0])!; + expect(secondRoutes).toBeDefined(); + expect(secondRoutes[0].path).toEqual('LoadedModule2'); + })); - it('should not preload when canLoad is present and does not execute guard', - fakeAsync(() => { - const router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmp); + it('should not preload when canLoad is present and does not execute guard', fakeAsync(() => { + const router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmp); - router.resetConfig([ - {path: 'blank', component: BlankCmp}, - {path: 'lazy', loadChildren: () => LoadedModule1, canLoad: ['loggingReturnsTrue']} - ]); + router.resetConfig([ + {path: 'blank', component: BlankCmp}, + {path: 'lazy', loadChildren: () => LoadedModule1, canLoad: ['loggingReturnsTrue']}, + ]); - router.navigateByUrl('/blank'); - advance(fixture); + router.navigateByUrl('/blank'); + advance(fixture); - const config = router.config; - const firstRoutes = getLoadedRoutes(config[1])!; + const config = router.config; + const firstRoutes = getLoadedRoutes(config[1])!; - expect(firstRoutes).toBeUndefined(); - expect(log.length).toBe(0); - })); + expect(firstRoutes).toBeUndefined(); + expect(log.length).toBe(0); + })); it('should allow navigation to modules with no routes', fakeAsync(() => { - const router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmp); + const router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'lazy', loadChildren: () => EmptyModule}]); + router.resetConfig([{path: 'lazy', loadChildren: () => EmptyModule}]); - router.navigateByUrl('/lazy'); - advance(fixture); - })); + router.navigateByUrl('/lazy'); + advance(fixture); + })); }); describe('custom url handling strategies', () => { @@ -6496,217 +6992,241 @@ for (const browserAPI of ['navigation', 'history'] as const) { TestBed.configureTestingModule({ providers: [ {provide: UrlHandlingStrategy, useClass: CustomUrlHandlingStrategy}, - {provide: LocationStrategy, useClass: HashLocationStrategy} - ] + {provide: LocationStrategy, useClass: HashLocationStrategy}, + ], }); }); - it('should work', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should work', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'include', - component: TeamCmp, - children: [ - {path: 'user/:name', component: UserCmp}, {path: 'simple', component: SimpleCmp} - ] - }]); + router.resetConfig([ + { + path: 'include', + component: TeamCmp, + children: [ + {path: 'user/:name', component: UserCmp}, + {path: 'simple', component: SimpleCmp}, + ], + }, + ]); - const events: Event[] = []; - router.events.subscribe(e => e instanceof RouterEvent && events.push(e)); + const events: Event[] = []; + router.events.subscribe((e) => e instanceof RouterEvent && events.push(e)); - // supported URL - router.navigateByUrl('/include/user/kate'); - advance(fixture); + // supported URL + router.navigateByUrl('/include/user/kate'); + advance(fixture); - expect(location.path()).toEqual('/include/user/kate'); - expectEvents(events, [ - [NavigationStart, '/include/user/kate'], [RoutesRecognized, '/include/user/kate'], - [GuardsCheckStart, '/include/user/kate'], [GuardsCheckEnd, '/include/user/kate'], - [ResolveStart, '/include/user/kate'], [ResolveEnd, '/include/user/kate'], - [NavigationEnd, '/include/user/kate'] - ]); - expect(fixture.nativeElement).toHaveText('team [ user kate, right: ]'); - events.splice(0); + expect(location.path()).toEqual('/include/user/kate'); + expectEvents(events, [ + [NavigationStart, '/include/user/kate'], + [RoutesRecognized, '/include/user/kate'], + [GuardsCheckStart, '/include/user/kate'], + [GuardsCheckEnd, '/include/user/kate'], + [ResolveStart, '/include/user/kate'], + [ResolveEnd, '/include/user/kate'], + [NavigationEnd, '/include/user/kate'], + ]); + expect(fixture.nativeElement).toHaveText('team [ user kate, right: ]'); + events.splice(0); - // unsupported URL - router.navigateByUrl('/exclude/one'); - advance(fixture); + // unsupported URL + router.navigateByUrl('/exclude/one'); + advance(fixture); - expect(location.path()).toEqual('/exclude/one'); - expect(Object.keys(router.routerState.root.children).length).toEqual(0); - expect(fixture.nativeElement).toHaveText(''); - expectEvents(events, [ - [NavigationStart, '/exclude/one'], [GuardsCheckStart, '/exclude/one'], - [GuardsCheckEnd, '/exclude/one'], [NavigationEnd, '/exclude/one'] - ]); - events.splice(0); + expect(location.path()).toEqual('/exclude/one'); + expect(Object.keys(router.routerState.root.children).length).toEqual(0); + expect(fixture.nativeElement).toHaveText(''); + expectEvents(events, [ + [NavigationStart, '/exclude/one'], + [GuardsCheckStart, '/exclude/one'], + [GuardsCheckEnd, '/exclude/one'], + [NavigationEnd, '/exclude/one'], + ]); + events.splice(0); - // another unsupported URL - location.go('/exclude/two'); - location.historyGo(0); - advance(fixture); + // another unsupported URL + location.go('/exclude/two'); + location.historyGo(0); + advance(fixture); - expect(location.path()).toEqual('/exclude/two'); - expectEvents(events, [[NavigationSkipped, '/exclude/two']]); - events.splice(0); + expect(location.path()).toEqual('/exclude/two'); + expectEvents(events, [[NavigationSkipped, '/exclude/two']]); + events.splice(0); - // back to a supported URL - location.go('/include/simple'); - location.historyGo(0); - advance(fixture); + // back to a supported URL + location.go('/include/simple'); + location.historyGo(0); + advance(fixture); - expect(location.path()).toEqual('/include/simple'); - expect(fixture.nativeElement).toHaveText('team [ simple, right: ]'); + expect(location.path()).toEqual('/include/simple'); + expect(fixture.nativeElement).toHaveText('team [ simple, right: ]'); - expectEvents(events, [ - [NavigationStart, '/include/simple'], [RoutesRecognized, '/include/simple'], - [GuardsCheckStart, '/include/simple'], [GuardsCheckEnd, '/include/simple'], - [ResolveStart, '/include/simple'], [ResolveEnd, '/include/simple'], - [NavigationEnd, '/include/simple'] - ]); - }))); + expectEvents(events, [ + [NavigationStart, '/include/simple'], + [RoutesRecognized, '/include/simple'], + [GuardsCheckStart, '/include/simple'], + [GuardsCheckEnd, '/include/simple'], + [ResolveStart, '/include/simple'], + [ResolveEnd, '/include/simple'], + [NavigationEnd, '/include/simple'], + ]); + }), + )); - it('should handle the case when the router takes only the primary url', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should handle the case when the router takes only the primary url', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'include', - component: TeamCmp, - children: [ - {path: 'user/:name', component: UserCmp}, {path: 'simple', component: SimpleCmp} - ] - }]); + router.resetConfig([ + { + path: 'include', + component: TeamCmp, + children: [ + {path: 'user/:name', component: UserCmp}, + {path: 'simple', component: SimpleCmp}, + ], + }, + ]); - const events: Event[] = []; - router.events.subscribe(e => e instanceof RouterEvent && events.push(e)); + const events: Event[] = []; + router.events.subscribe((e) => e instanceof RouterEvent && events.push(e)); - location.go('/include/user/kate(aux:excluded)'); - location.historyGo(0); - advance(fixture); + location.go('/include/user/kate(aux:excluded)'); + location.historyGo(0); + advance(fixture); - expect(location.path()).toEqual('/include/user/kate(aux:excluded)'); - expectEvents(events, [ - [NavigationStart, '/include/user/kate'], [RoutesRecognized, '/include/user/kate'], - [GuardsCheckStart, '/include/user/kate'], [GuardsCheckEnd, '/include/user/kate'], - [ResolveStart, '/include/user/kate'], [ResolveEnd, '/include/user/kate'], - [NavigationEnd, '/include/user/kate'] - ]); - events.splice(0); + expect(location.path()).toEqual('/include/user/kate(aux:excluded)'); + expectEvents(events, [ + [NavigationStart, '/include/user/kate'], + [RoutesRecognized, '/include/user/kate'], + [GuardsCheckStart, '/include/user/kate'], + [GuardsCheckEnd, '/include/user/kate'], + [ResolveStart, '/include/user/kate'], + [ResolveEnd, '/include/user/kate'], + [NavigationEnd, '/include/user/kate'], + ]); + events.splice(0); - location.go('/include/user/kate(aux:excluded2)'); - location.historyGo(0); - advance(fixture); - expectEvents(events, [[NavigationSkipped, '/include/user/kate(aux:excluded2)']]); - events.splice(0); + location.go('/include/user/kate(aux:excluded2)'); + location.historyGo(0); + advance(fixture); + expectEvents(events, [[NavigationSkipped, '/include/user/kate(aux:excluded2)']]); + events.splice(0); - router.navigateByUrl('/include/simple'); - advance(fixture); + router.navigateByUrl('/include/simple'); + advance(fixture); - expect(location.path()).toEqual('/include/simple(aux:excluded2)'); - expectEvents(events, [ - [NavigationStart, '/include/simple'], [RoutesRecognized, '/include/simple'], - [GuardsCheckStart, '/include/simple'], [GuardsCheckEnd, '/include/simple'], - [ResolveStart, '/include/simple'], [ResolveEnd, '/include/simple'], - [NavigationEnd, '/include/simple'] - ]); - }))); + expect(location.path()).toEqual('/include/simple(aux:excluded2)'); + expectEvents(events, [ + [NavigationStart, '/include/simple'], + [RoutesRecognized, '/include/simple'], + [GuardsCheckStart, '/include/simple'], + [GuardsCheckEnd, '/include/simple'], + [ResolveStart, '/include/simple'], + [ResolveEnd, '/include/simple'], + [NavigationEnd, '/include/simple'], + ]); + }), + )); - it('should not remove parts of the URL that are not handled by the router when "eager"', - fakeAsync(() => { - TestBed.configureTestingModule( - {providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))]}); - const router = TestBed.inject(Router); - const location = TestBed.inject(Location); - const fixture = createRoot(router, RootCmp); + it('should not remove parts of the URL that are not handled by the router when "eager"', fakeAsync(() => { + TestBed.configureTestingModule({ + providers: [provideRouter([], withRouterConfig({urlUpdateStrategy: 'eager'}))], + }); + const router = TestBed.inject(Router); + const location = TestBed.inject(Location); + const fixture = createRoot(router, RootCmp); - router.resetConfig([{ - path: 'include', - component: TeamCmp, - children: [{path: 'user/:name', component: UserCmp}] - }]); + router.resetConfig([ + { + path: 'include', + component: TeamCmp, + children: [{path: 'user/:name', component: UserCmp}], + }, + ]); - location.go('/include/user/kate(aux:excluded)'); - location.historyGo(0); - advance(fixture); + location.go('/include/user/kate(aux:excluded)'); + location.historyGo(0); + advance(fixture); - expect(location.path()).toEqual('/include/user/kate(aux:excluded)'); - })); + expect(location.path()).toEqual('/include/user/kate(aux:excluded)'); + })); }); - it('can use `relativeTo` `route.parent` in `routerLink` to close secondary outlet', - fakeAsync(() => { - // Given - @Component({template: ''}) - class ChildRootCmp { - } + it('can use `relativeTo` `route.parent` in `routerLink` to close secondary outlet', fakeAsync(() => { + // Given + @Component({template: ''}) + class ChildRootCmp {} - @Component({ - selector: 'link-cmp', - template: - `link + @Component({ + selector: 'link-cmp', + template: `link - ` - }) - class RelativeLinkCmp { - @ViewChildren(RouterLink) links!: QueryList; + `, + }) + class RelativeLinkCmp { + @ViewChildren(RouterLink) links!: QueryList; - constructor(readonly route: ActivatedRoute) {} - } - @NgModule({ - declarations: [RelativeLinkCmp, ChildRootCmp], - imports: [RouterModule.forChild([{ - path: 'childRoot', - component: ChildRootCmp, - children: [ - {path: 'popup', outlet: 'secondary', component: RelativeLinkCmp}, - ] - }])] - }) - class LazyLoadedModule { - } - const router = TestBed.inject(Router); - router.resetConfig([{path: 'root', loadChildren: () => LazyLoadedModule}]); + constructor(readonly route: ActivatedRoute) {} + } + @NgModule({ + declarations: [RelativeLinkCmp, ChildRootCmp], + imports: [ + RouterModule.forChild([ + { + path: 'childRoot', + component: ChildRootCmp, + children: [{path: 'popup', outlet: 'secondary', component: RelativeLinkCmp}], + }, + ]), + ], + }) + class LazyLoadedModule {} + const router = TestBed.inject(Router); + router.resetConfig([{path: 'root', loadChildren: () => LazyLoadedModule}]); - // When - router.navigateByUrl('/root/childRoot/(secondary:popup)'); - const fixture = createRoot(router, RootCmp); - advance(fixture); + // When + router.navigateByUrl('/root/childRoot/(secondary:popup)'); + const fixture = createRoot(router, RootCmp); + advance(fixture); - // Then - const relativeLinkCmp = - fixture.debugElement.query(By.directive(RelativeLinkCmp)).componentInstance; - expect(relativeLinkCmp.links.first.urlTree.toString()).toEqual('/root/childRoot'); - expect(relativeLinkCmp.links.last.urlTree.toString()).toEqual('/root/childRoot'); - })); + // Then + const relativeLinkCmp = fixture.debugElement.query( + By.directive(RelativeLinkCmp), + ).componentInstance; + expect(relativeLinkCmp.links.first.urlTree.toString()).toEqual('/root/childRoot'); + expect(relativeLinkCmp.links.last.urlTree.toString()).toEqual('/root/childRoot'); + })); - it('should ignore empty path for relative links', - fakeAsync(inject([Router], (router: Router) => { - @Component({selector: 'link-cmp', template: `link`}) - class RelativeLinkCmp { - } + it('should ignore empty path for relative links', fakeAsync( + inject([Router], (router: Router) => { + @Component({selector: 'link-cmp', template: `link`}) + class RelativeLinkCmp {} - @NgModule({ - declarations: [RelativeLinkCmp], - imports: [RouterModule.forChild([ - {path: 'foo/bar', children: [{path: '', component: RelativeLinkCmp}]}, - ])] - }) - class LazyLoadedModule { - } + @NgModule({ + declarations: [RelativeLinkCmp], + imports: [ + RouterModule.forChild([ + {path: 'foo/bar', children: [{path: '', component: RelativeLinkCmp}]}, + ]), + ], + }) + class LazyLoadedModule {} - const fixture = createRoot(router, RootCmp); + const fixture = createRoot(router, RootCmp); - router.resetConfig([{path: 'lazy', loadChildren: () => LazyLoadedModule}]); + router.resetConfig([{path: 'lazy', loadChildren: () => LazyLoadedModule}]); - router.navigateByUrl('/lazy/foo/bar'); - advance(fixture); + router.navigateByUrl('/lazy/foo/bar'); + advance(fixture); - const link = fixture.nativeElement.querySelector('a'); - expect(link.getAttribute('href')).toEqual('/lazy/foo/simple'); - }))); + const link = fixture.nativeElement.querySelector('a'); + expect(link.getAttribute('href')).toEqual('/lazy/foo/simple'); + }), + )); }); describe('Custom Route Reuse Strategy', () => { @@ -6715,8 +7235,10 @@ for (const browserAPI of ['navigation', 'history'] as const) { pathsToDetach = ['a']; shouldDetach(route: ActivatedRouteSnapshot): boolean { - return typeof route.routeConfig!.path !== 'undefined' && - this.pathsToDetach.includes(route.routeConfig!.path); + return ( + typeof route.routeConfig!.path !== 'undefined' && + this.pathsToDetach.includes(route.routeConfig!.path) + ); } store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void { @@ -6744,7 +7266,7 @@ for (const browserAPI of ['navigation', 'history'] as const) { shouldAttach(route: ActivatedRouteSnapshot): boolean { return false; } - retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle|null { + retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null { return null; } shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { @@ -6756,7 +7278,7 @@ for (const browserAPI of ['navigation', 'history'] as const) { return false; } - return Object.keys(future.params).every(k => future.params[k] === curr.params[k]); + return Object.keys(future.params).every((k) => future.params[k] === curr.params[k]); } } @@ -6765,7 +7287,7 @@ for (const browserAPI of ['navigation', 'history'] as const) { providers: [ {provide: RouteReuseStrategy, useClass: AttachDetachReuseStrategy}, provideRouter([]), - ] + ], }); const router = TestBed.inject(Router); @@ -6774,374 +7296,362 @@ for (const browserAPI of ['navigation', 'history'] as const) { }); it('should emit an event when an outlet gets attached/detached', fakeAsync(() => { - @Component({ - selector: 'container', - template: - `` - }) - class Container { - attachedComponents: unknown[] = []; - detachedComponents: unknown[] = []; + @Component({ + selector: 'container', + template: ``, + }) + class Container { + attachedComponents: unknown[] = []; + detachedComponents: unknown[] = []; - recordAttached(component: unknown): void { - this.attachedComponents.push(component); - } + recordAttached(component: unknown): void { + this.attachedComponents.push(component); + } - recordDetached(component: unknown): void { - this.detachedComponents.push(component); - } - } + recordDetached(component: unknown): void { + this.detachedComponents.push(component); + } + } - TestBed.configureTestingModule({ - declarations: [Container], - providers: [{provide: RouteReuseStrategy, useClass: AttachDetachReuseStrategy}] - }); + TestBed.configureTestingModule({ + declarations: [Container], + providers: [{provide: RouteReuseStrategy, useClass: AttachDetachReuseStrategy}], + }); - const router = TestBed.inject(Router); - const fixture = createRoot(router, Container); - const cmp = fixture.componentInstance; + const router = TestBed.inject(Router); + const fixture = createRoot(router, Container); + const cmp = fixture.componentInstance; - router.resetConfig( - [{path: 'a', component: BlankCmp}, {path: 'b', component: SimpleCmp}]); + router.resetConfig([ + {path: 'a', component: BlankCmp}, + {path: 'b', component: SimpleCmp}, + ]); - cmp.attachedComponents = []; - cmp.detachedComponents = []; + cmp.attachedComponents = []; + cmp.detachedComponents = []; - router.navigateByUrl('/a'); - advance(fixture); - expect(cmp.attachedComponents.length).toEqual(0); - expect(cmp.detachedComponents.length).toEqual(0); + router.navigateByUrl('/a'); + advance(fixture); + expect(cmp.attachedComponents.length).toEqual(0); + expect(cmp.detachedComponents.length).toEqual(0); - router.navigateByUrl('/b'); - advance(fixture); - expect(cmp.attachedComponents.length).toEqual(0); - expect(cmp.detachedComponents.length).toEqual(1); - expect(cmp.detachedComponents[0] instanceof BlankCmp).toBe(true); + router.navigateByUrl('/b'); + advance(fixture); + expect(cmp.attachedComponents.length).toEqual(0); + expect(cmp.detachedComponents.length).toEqual(1); + expect(cmp.detachedComponents[0] instanceof BlankCmp).toBe(true); - // the route will be reused by the `RouteReuseStrategy` - router.navigateByUrl('/a'); - advance(fixture); - expect(cmp.attachedComponents.length).toEqual(1); - expect(cmp.attachedComponents[0] instanceof BlankCmp).toBe(true); - expect(cmp.detachedComponents.length).toEqual(1); - expect(cmp.detachedComponents[0] instanceof BlankCmp).toBe(true); - })); + // the route will be reused by the `RouteReuseStrategy` + router.navigateByUrl('/a'); + advance(fixture); + expect(cmp.attachedComponents.length).toEqual(1); + expect(cmp.attachedComponents[0] instanceof BlankCmp).toBe(true); + expect(cmp.detachedComponents.length).toEqual(1); + expect(cmp.detachedComponents[0] instanceof BlankCmp).toBe(true); + })); - it('should support attaching & detaching fragments', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); + it('should support attaching & detaching fragments', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); - router.routeReuseStrategy = new AttachDetachReuseStrategy(); - (router.routeReuseStrategy as AttachDetachReuseStrategy).pathsToDetach = ['a', 'b']; - spyOn(router.routeReuseStrategy, 'retrieve').and.callThrough(); + router.routeReuseStrategy = new AttachDetachReuseStrategy(); + (router.routeReuseStrategy as AttachDetachReuseStrategy).pathsToDetach = ['a', 'b']; + spyOn(router.routeReuseStrategy, 'retrieve').and.callThrough(); - router.resetConfig([ - { - path: 'a', - component: TeamCmp, - children: [{path: 'b', component: SimpleCmp}], - }, - {path: 'c', component: UserCmp}, - ]); + router.resetConfig([ + { + path: 'a', + component: TeamCmp, + children: [{path: 'b', component: SimpleCmp}], + }, + {path: 'c', component: UserCmp}, + ]); - router.navigateByUrl('/a/b'); - advance(fixture); - const teamCmp = fixture.debugElement.children[1].componentInstance; - const simpleCmp = fixture.debugElement.children[1].children[1].componentInstance; - expect(location.path()).toEqual('/a/b'); - expect(teamCmp).toBeDefined(); - expect(simpleCmp).toBeDefined(); - expect(router.routeReuseStrategy.retrieve).not.toHaveBeenCalled(); + router.navigateByUrl('/a/b'); + advance(fixture); + const teamCmp = fixture.debugElement.children[1].componentInstance; + const simpleCmp = fixture.debugElement.children[1].children[1].componentInstance; + expect(location.path()).toEqual('/a/b'); + expect(teamCmp).toBeDefined(); + expect(simpleCmp).toBeDefined(); + expect(router.routeReuseStrategy.retrieve).not.toHaveBeenCalled(); - router.navigateByUrl('/c'); - advance(fixture); - expect(location.path()).toEqual('/c'); - expect(fixture.debugElement.children[1].componentInstance).toBeInstanceOf(UserCmp); - // We have still not encountered a route that should be reattached - expect(router.routeReuseStrategy.retrieve).not.toHaveBeenCalled(); + router.navigateByUrl('/c'); + advance(fixture); + expect(location.path()).toEqual('/c'); + expect(fixture.debugElement.children[1].componentInstance).toBeInstanceOf(UserCmp); + // We have still not encountered a route that should be reattached + expect(router.routeReuseStrategy.retrieve).not.toHaveBeenCalled(); - router.navigateByUrl('/a;p=1/b;p=2'); - advance(fixture); - // We retrieve both the stored route snapshots - expect(router.routeReuseStrategy.retrieve).toHaveBeenCalledTimes(4); - const teamCmp2 = fixture.debugElement.children[1].componentInstance; - const simpleCmp2 = fixture.debugElement.children[1].children[1].componentInstance; - expect(location.path()).toEqual('/a;p=1/b;p=2'); - expect(teamCmp2).toBe(teamCmp); - expect(simpleCmp2).toBe(simpleCmp); + router.navigateByUrl('/a;p=1/b;p=2'); + advance(fixture); + // We retrieve both the stored route snapshots + expect(router.routeReuseStrategy.retrieve).toHaveBeenCalledTimes(4); + const teamCmp2 = fixture.debugElement.children[1].componentInstance; + const simpleCmp2 = fixture.debugElement.children[1].children[1].componentInstance; + expect(location.path()).toEqual('/a;p=1/b;p=2'); + expect(teamCmp2).toBe(teamCmp); + expect(simpleCmp2).toBe(simpleCmp); - expect(teamCmp.route).toBe(router.routerState.root.firstChild); - expect(teamCmp.route.snapshot).toBe(router.routerState.snapshot.root.firstChild); - expect(teamCmp.route.snapshot.params).toEqual({p: '1'}); - expect(teamCmp.route.firstChild.snapshot.params).toEqual({p: '2'}); - expect(teamCmp.recordedParams).toEqual([{}, {p: '1'}]); - }))); + expect(teamCmp.route).toBe(router.routerState.root.firstChild); + expect(teamCmp.route.snapshot).toBe(router.routerState.snapshot.root.firstChild); + expect(teamCmp.route.snapshot.params).toEqual({p: '1'}); + expect(teamCmp.route.firstChild.snapshot.params).toEqual({p: '2'}); + expect(teamCmp.recordedParams).toEqual([{}, {p: '1'}]); + }), + )); - it('should support shorter lifecycles', - fakeAsync(inject([Router, Location], (router: Router, location: Location) => { - const fixture = createRoot(router, RootCmp); - router.routeReuseStrategy = new ShortLifecycle(); + it('should support shorter lifecycles', fakeAsync( + inject([Router, Location], (router: Router, location: Location) => { + const fixture = createRoot(router, RootCmp); + router.routeReuseStrategy = new ShortLifecycle(); - router.resetConfig([{path: 'a', component: SimpleCmp}]); + router.resetConfig([{path: 'a', component: SimpleCmp}]); - router.navigateByUrl('/a'); - advance(fixture); - const simpleCmp1 = fixture.debugElement.children[1].componentInstance; - expect(location.path()).toEqual('/a'); + router.navigateByUrl('/a'); + advance(fixture); + const simpleCmp1 = fixture.debugElement.children[1].componentInstance; + expect(location.path()).toEqual('/a'); - router.navigateByUrl('/a;p=1'); - advance(fixture); - expect(location.path()).toEqual('/a;p=1'); - const simpleCmp2 = fixture.debugElement.children[1].componentInstance; - expect(simpleCmp1).not.toBe(simpleCmp2); - }))); + router.navigateByUrl('/a;p=1'); + advance(fixture); + expect(location.path()).toEqual('/a;p=1'); + const simpleCmp2 = fixture.debugElement.children[1].componentInstance; + expect(simpleCmp1).not.toBe(simpleCmp2); + }), + )); - it('should not mount the component of the previously reused route when the outlet was not instantiated at the time of route activation', - fakeAsync(() => { - @Component({ - selector: 'root-cmp', - template: - '
' - }) - class RootCmpWithCondOutlet implements OnDestroy { - private subscription: Subscription; - public isToolpanelShowing: boolean = false; + it('should not mount the component of the previously reused route when the outlet was not instantiated at the time of route activation', fakeAsync(() => { + @Component({ + selector: 'root-cmp', + template: + '
', + }) + class RootCmpWithCondOutlet implements OnDestroy { + private subscription: Subscription; + public isToolpanelShowing: boolean = false; - constructor(router: Router) { - this.subscription = - router.events.pipe(filter(event => event instanceof NavigationEnd)) - .subscribe( - () => this.isToolpanelShowing = - !!router.parseUrl(router.url).root.children['toolpanel']); - } + constructor(router: Router) { + this.subscription = router.events + .pipe(filter((event) => event instanceof NavigationEnd)) + .subscribe( + () => + (this.isToolpanelShowing = !!router.parseUrl(router.url).root.children[ + 'toolpanel' + ]), + ); + } - public ngOnDestroy(): void { - this.subscription.unsubscribe(); - } - } + public ngOnDestroy(): void { + this.subscription.unsubscribe(); + } + } - @Component({selector: 'tool-1-cmp', template: 'Tool 1 showing'}) - class Tool1Component { - } + @Component({selector: 'tool-1-cmp', template: 'Tool 1 showing'}) + class Tool1Component {} - @Component({selector: 'tool-2-cmp', template: 'Tool 2 showing'}) - class Tool2Component { - } + @Component({selector: 'tool-2-cmp', template: 'Tool 2 showing'}) + class Tool2Component {} - @NgModule({ - declarations: [RootCmpWithCondOutlet, Tool1Component, Tool2Component], - imports: [CommonModule, ...ROUTER_DIRECTIVES], - providers: [ - provideRouter([ - {path: 'a', outlet: 'toolpanel', component: Tool1Component}, - {path: 'b', outlet: 'toolpanel', component: Tool2Component}, - ]), - ] - }) - class TestModule { - } + @NgModule({ + declarations: [RootCmpWithCondOutlet, Tool1Component, Tool2Component], + imports: [CommonModule, ...ROUTER_DIRECTIVES], + providers: [ + provideRouter([ + {path: 'a', outlet: 'toolpanel', component: Tool1Component}, + {path: 'b', outlet: 'toolpanel', component: Tool2Component}, + ]), + ], + }) + class TestModule {} - TestBed.configureTestingModule({imports: [TestModule]}); + TestBed.configureTestingModule({imports: [TestModule]}); - const router: Router = TestBed.inject(Router); - router.routeReuseStrategy = new AttachDetachReuseStrategy(); + const router: Router = TestBed.inject(Router); + router.routeReuseStrategy = new AttachDetachReuseStrategy(); - const fixture = createRoot(router, RootCmpWithCondOutlet); + const fixture = createRoot(router, RootCmpWithCondOutlet); - // Activate 'tool-1' - router.navigate([{outlets: {toolpanel: 'a'}}]); - advance(fixture); - expect(fixture).toContainComponent(Tool1Component, '(a)'); + // Activate 'tool-1' + router.navigate([{outlets: {toolpanel: 'a'}}]); + advance(fixture); + expect(fixture).toContainComponent(Tool1Component, '(a)'); - // Deactivate 'tool-1' - router.navigate([{outlets: {toolpanel: null}}]); - advance(fixture); - expect(fixture).not.toContainComponent(Tool1Component, '(b)'); + // Deactivate 'tool-1' + router.navigate([{outlets: {toolpanel: null}}]); + advance(fixture); + expect(fixture).not.toContainComponent(Tool1Component, '(b)'); - // Activate 'tool-1' - router.navigate([{outlets: {toolpanel: 'a'}}]); - advance(fixture); - expect(fixture).toContainComponent(Tool1Component, '(c)'); + // Activate 'tool-1' + router.navigate([{outlets: {toolpanel: 'a'}}]); + advance(fixture); + expect(fixture).toContainComponent(Tool1Component, '(c)'); - // Deactivate 'tool-1' - router.navigate([{outlets: {toolpanel: null}}]); - advance(fixture); - expect(fixture).not.toContainComponent(Tool1Component, '(d)'); + // Deactivate 'tool-1' + router.navigate([{outlets: {toolpanel: null}}]); + advance(fixture); + expect(fixture).not.toContainComponent(Tool1Component, '(d)'); - // Activate 'tool-2' - router.navigate([{outlets: {toolpanel: 'b'}}]); - advance(fixture); - expect(fixture).toContainComponent(Tool2Component, '(e)'); - })); + // Activate 'tool-2' + router.navigate([{outlets: {toolpanel: 'b'}}]); + advance(fixture); + expect(fixture).toContainComponent(Tool2Component, '(e)'); + })); it('should not remount a destroyed component', fakeAsync(() => { - @Component({ - selector: 'root-cmp', - template: '
' - }) - class RootCmpWithCondOutlet { - public showRouterOutlet: boolean = true; - } + @Component({ + selector: 'root-cmp', + template: '
', + }) + class RootCmpWithCondOutlet { + public showRouterOutlet: boolean = true; + } - @NgModule({ - declarations: [RootCmpWithCondOutlet], - imports: [ - CommonModule, - ...ROUTER_DIRECTIVES, - ], - providers: [ - {provide: RouteReuseStrategy, useClass: AttachDetachReuseStrategy}, - provideRouter([ - {path: 'a', component: SimpleCmp}, - {path: 'b', component: BlankCmp}, - ]), - ] - }) - class TestModule { - } - TestBed.configureTestingModule({imports: [TestModule]}); + @NgModule({ + declarations: [RootCmpWithCondOutlet], + imports: [CommonModule, ...ROUTER_DIRECTIVES], + providers: [ + {provide: RouteReuseStrategy, useClass: AttachDetachReuseStrategy}, + provideRouter([ + {path: 'a', component: SimpleCmp}, + {path: 'b', component: BlankCmp}, + ]), + ], + }) + class TestModule {} + TestBed.configureTestingModule({imports: [TestModule]}); - const router: Router = TestBed.inject(Router); - const fixture = createRoot(router, RootCmpWithCondOutlet); + const router: Router = TestBed.inject(Router); + const fixture = createRoot(router, RootCmpWithCondOutlet); - // Activate 'a' - router.navigate(['a']); - advance(fixture); - expect(fixture.debugElement.query(By.directive(SimpleCmp))).toBeTruthy(); + // Activate 'a' + router.navigate(['a']); + advance(fixture); + expect(fixture.debugElement.query(By.directive(SimpleCmp))).toBeTruthy(); - // Deactivate 'a' and detach the route - router.navigate(['b']); - advance(fixture); - expect(fixture.debugElement.query(By.directive(SimpleCmp))).toBeNull(); + // Deactivate 'a' and detach the route + router.navigate(['b']); + advance(fixture); + expect(fixture.debugElement.query(By.directive(SimpleCmp))).toBeNull(); - // Activate 'a' again, the route should be re-attached - router.navigate(['a']); - advance(fixture); - expect(fixture.debugElement.query(By.directive(SimpleCmp))).toBeTruthy(); + // Activate 'a' again, the route should be re-attached + router.navigate(['a']); + advance(fixture); + expect(fixture.debugElement.query(By.directive(SimpleCmp))).toBeTruthy(); - // Hide the router-outlet, SimpleCmp should be destroyed - fixture.componentInstance.showRouterOutlet = false; - advance(fixture); - expect(fixture.debugElement.query(By.directive(SimpleCmp))).toBeNull(); + // Hide the router-outlet, SimpleCmp should be destroyed + fixture.componentInstance.showRouterOutlet = false; + advance(fixture); + expect(fixture.debugElement.query(By.directive(SimpleCmp))).toBeNull(); - // Show the router-outlet, SimpleCmp should be re-created - fixture.componentInstance.showRouterOutlet = true; - advance(fixture); - expect(fixture.debugElement.query(By.directive(SimpleCmp))).toBeTruthy(); - })); + // Show the router-outlet, SimpleCmp should be re-created + fixture.componentInstance.showRouterOutlet = true; + advance(fixture); + expect(fixture.debugElement.query(By.directive(SimpleCmp))).toBeTruthy(); + })); it('should allow to attach parent route with fresh child route', fakeAsync(() => { - const CREATED_COMPS = new InjectionToken('CREATED_COMPS'); + const CREATED_COMPS = new InjectionToken('CREATED_COMPS'); - @Component({selector: 'root', template: ``}) - class Root { - } + @Component({selector: 'root', template: ``}) + class Root {} - @Component({selector: 'parent', template: ``}) - class Parent { - constructor(@Inject(CREATED_COMPS) createdComps: string[]) { - createdComps.push('parent'); - } - } + @Component({selector: 'parent', template: ``}) + class Parent { + constructor(@Inject(CREATED_COMPS) createdComps: string[]) { + createdComps.push('parent'); + } + } - @Component({selector: 'child', template: `child`}) - class Child { - constructor(@Inject(CREATED_COMPS) createdComps: string[]) { - createdComps.push('child'); - } - } + @Component({selector: 'child', template: `child`}) + class Child { + constructor(@Inject(CREATED_COMPS) createdComps: string[]) { + createdComps.push('child'); + } + } - @NgModule({ - declarations: [Root, Parent, Child], - imports: [ - CommonModule, - ...ROUTER_DIRECTIVES, - ], - providers: [ - {provide: RouteReuseStrategy, useClass: AttachDetachReuseStrategy}, - {provide: CREATED_COMPS, useValue: []}, - provideRouter([ - {path: 'a', component: Parent, children: [{path: 'b', component: Child}]}, - {path: 'c', component: SimpleCmp} - ]), - ] - }) - class TestModule { - } - TestBed.configureTestingModule({imports: [TestModule]}); + @NgModule({ + declarations: [Root, Parent, Child], + imports: [CommonModule, ...ROUTER_DIRECTIVES], + providers: [ + {provide: RouteReuseStrategy, useClass: AttachDetachReuseStrategy}, + {provide: CREATED_COMPS, useValue: []}, + provideRouter([ + {path: 'a', component: Parent, children: [{path: 'b', component: Child}]}, + {path: 'c', component: SimpleCmp}, + ]), + ], + }) + class TestModule {} + TestBed.configureTestingModule({imports: [TestModule]}); - const router = TestBed.inject(Router); - const fixture = createRoot(router, Root); - const createdComps = TestBed.inject(CREATED_COMPS); + const router = TestBed.inject(Router); + const fixture = createRoot(router, Root); + const createdComps = TestBed.inject(CREATED_COMPS); - expect(createdComps).toEqual([]); + expect(createdComps).toEqual([]); - router.navigateByUrl('/a/b'); - advance(fixture); - expect(createdComps).toEqual(['parent', 'child']); + router.navigateByUrl('/a/b'); + advance(fixture); + expect(createdComps).toEqual(['parent', 'child']); - router.navigateByUrl('/c'); - advance(fixture); - expect(createdComps).toEqual(['parent', 'child']); + router.navigateByUrl('/c'); + advance(fixture); + expect(createdComps).toEqual(['parent', 'child']); - // 'a' parent route will be reused by the `RouteReuseStrategy`, child 'b' should be - // recreated - router.navigateByUrl('/a/b'); - advance(fixture); - expect(createdComps).toEqual(['parent', 'child', 'child']); - })); + // 'a' parent route will be reused by the `RouteReuseStrategy`, child 'b' should be + // recreated + router.navigateByUrl('/a/b'); + advance(fixture); + expect(createdComps).toEqual(['parent', 'child', 'child']); + })); - it('should not try to detach the outlet of a route that does not get to attach a component', - fakeAsync(() => { - @Component({selector: 'root', template: ``}) - class Root { - } + it('should not try to detach the outlet of a route that does not get to attach a component', fakeAsync(() => { + @Component({selector: 'root', template: ``}) + class Root {} - @Component({selector: 'component-a', template: 'Component A'}) - class ComponentA { - } + @Component({selector: 'component-a', template: 'Component A'}) + class ComponentA {} - @Component({selector: 'component-b', template: 'Component B'}) - class ComponentB { - } + @Component({selector: 'component-b', template: 'Component B'}) + class ComponentB {} - @NgModule({ - declarations: [ComponentA], - imports: [RouterModule.forChild([{path: '', component: ComponentA}])], - }) - class LoadedModule { - } + @NgModule({ + declarations: [ComponentA], + imports: [RouterModule.forChild([{path: '', component: ComponentA}])], + }) + class LoadedModule {} - @NgModule({ - declarations: [Root, ComponentB], - imports: [ROUTER_DIRECTIVES], - providers: [ - {provide: RouteReuseStrategy, useClass: AttachDetachReuseStrategy}, - provideRouter([ - {path: 'a', loadChildren: () => LoadedModule}, {path: 'b', component: ComponentB} - ]), - ] - }) - class TestModule { - } + @NgModule({ + declarations: [Root, ComponentB], + imports: [ROUTER_DIRECTIVES], + providers: [ + {provide: RouteReuseStrategy, useClass: AttachDetachReuseStrategy}, + provideRouter([ + {path: 'a', loadChildren: () => LoadedModule}, + {path: 'b', component: ComponentB}, + ]), + ], + }) + class TestModule {} - TestBed.configureTestingModule({imports: [TestModule]}); + TestBed.configureTestingModule({imports: [TestModule]}); - const router = TestBed.inject(Router); - const strategy = TestBed.inject(RouteReuseStrategy); - const fixture = createRoot(router, Root); + const router = TestBed.inject(Router); + const strategy = TestBed.inject(RouteReuseStrategy); + const fixture = createRoot(router, Root); - spyOn(strategy, 'shouldDetach').and.callThrough(); + spyOn(strategy, 'shouldDetach').and.callThrough(); - router.navigateByUrl('/a'); - advance(fixture); + router.navigateByUrl('/a'); + advance(fixture); - // Deactivate 'a' - // 'shouldDetach' should not be called for the componentless route - router.navigateByUrl('/b'); - advance(fixture); - expect(strategy.shouldDetach).toHaveBeenCalledTimes(1); - })); + // Deactivate 'a' + // 'shouldDetach' should not be called for the componentless route + router.navigateByUrl('/b'); + advance(fixture); + expect(strategy.shouldDetach).toHaveBeenCalledTimes(1); + })); }); }); } @@ -7149,37 +7659,35 @@ for (const browserAPI of ['navigation', 'history'] as const) { function expectEvents(events: Event[], pairs: any[]) { expect(events.length).toEqual(pairs.length); for (let i = 0; i < events.length; ++i) { - expect((events[i].constructor).name).toBe(pairs[i][0].name); + expect(events[i].constructor.name).toBe(pairs[i][0].name); expect((events[i]).url).toBe(pairs[i][1]); } } -function onlyNavigationStartAndEnd(e: Event): e is NavigationStart|NavigationEnd { +function onlyNavigationStartAndEnd(e: Event): e is NavigationStart | NavigationEnd { return e instanceof NavigationStart || e instanceof NavigationEnd; } -@Component( - {selector: 'link-cmp', template: `link`}) -class StringLinkCmp { -} +@Component({ + selector: 'link-cmp', + template: `link`, +}) +class StringLinkCmp {} @Component({selector: 'link-cmp', template: ``}) -class StringLinkButtonCmp { -} +class StringLinkButtonCmp {} @Component({ selector: 'link-cmp', - template: `link` + template: `link`, }) -class AbsoluteLinkCmp { -} +class AbsoluteLinkCmp {} @Component({ selector: 'link-cmp', - template: - `link + template: `link - ` + `, }) class DummyLinkCmp { private exact: boolean; @@ -7195,37 +7703,31 @@ class DummyLinkCmp { } @Component({selector: 'link-cmp', template: `link`}) -class AbsoluteSimpleLinkCmp { -} +class AbsoluteSimpleLinkCmp {} @Component({selector: 'link-cmp', template: `link`}) -class RelativeLinkCmp { -} +class RelativeLinkCmp {} @Component({ selector: 'link-cmp', - template: `link` + template: `link`, }) -class LinkWithQueryParamsAndFragment { -} +class LinkWithQueryParamsAndFragment {} @Component({ selector: 'link-cmp', - template: `link` + template: `link`, }) -class LinkWithState { -} +class LinkWithState {} @Component({ selector: 'div-link-cmp', - template: `` + template: ``, }) -class DivLinkWithState { -} +class DivLinkWithState {} @Component({selector: 'simple-cmp', template: `simple`}) -class SimpleCmp { -} +class SimpleCmp {} @Component({selector: 'collect-params-cmp', template: `collect-params`}) class CollectParamsCmp { @@ -7233,8 +7735,8 @@ class CollectParamsCmp { private urls: UrlSegment[][] = []; constructor(public route: ActivatedRoute) { - route.params.forEach(p => this.params.push(p)); - route.url.forEach(u => this.urls.push(u)); + route.params.forEach((p) => this.params.push(p)); + route.url.forEach((u) => this.urls.push(u)); } recordedUrls(): string[] { @@ -7243,19 +7745,18 @@ class CollectParamsCmp { } @Component({selector: 'blank-cmp', template: ``}) -class BlankCmp { -} +class BlankCmp {} @NgModule({imports: [RouterModule.forChild([{path: '', component: BlankCmp}])]}) -class ModuleWithBlankCmpAsRoute { -} +class ModuleWithBlankCmpAsRoute {} @Component({ selector: 'team-cmp', - template: `team {{id | async}} ` + - `[ , right: ]` + - `` + - `` + template: + `team {{id | async}} ` + + `[ , right: ]` + + `` + + ``, }) class TeamCmp { id: Observable; @@ -7265,7 +7766,7 @@ class TeamCmp { constructor(public route: ActivatedRoute) { this.id = route.params.pipe(map((p: Params) => p['id'])); - route.params.forEach(p => { + route.params.forEach((p) => { this.recordedParams.push(p); this.snapshotParams.push(route.snapshot.params); }); @@ -7274,11 +7775,9 @@ class TeamCmp { @Component({ selector: 'two-outlets-cmp', - template: `[ , aux: ]` + template: `[ , aux: ]`, }) -class TwoOutletsCmp { -} - +class TwoOutletsCmp {} @Component({selector: 'user-cmp', template: `user {{name | async}}`}) class UserCmp { @@ -7288,7 +7787,7 @@ class UserCmp { constructor(route: ActivatedRoute) { this.name = route.params.pipe(map((p: Params) => p['name'])); - route.params.forEach(p => { + route.params.forEach((p) => { this.recordedParams.push(p); this.snapshotParams.push(route.snapshot.params); }); @@ -7296,26 +7795,29 @@ class UserCmp { } @Component({selector: 'wrapper', template: ``}) -class WrapperCmp { -} +class WrapperCmp {} -@Component( - {selector: 'query-cmp', template: `query: {{name | async}} fragment: {{fragment | async}}`}) +@Component({ + selector: 'query-cmp', + template: `query: {{name | async}} fragment: {{fragment | async}}`, +}) class QueryParamsAndFragmentCmp { - name: Observable; + name: Observable; fragment: Observable; constructor(route: ActivatedRoute) { this.name = route.queryParamMap.pipe(map((p: ParamMap) => p.get('name'))); - this.fragment = route.fragment.pipe(map((p: string|null|undefined) => { - if (p === undefined) { - return 'undefined'; - } else if (p === null) { - return 'null'; - } else { - return p; - } - })); + this.fragment = route.fragment.pipe( + map((p: string | null | undefined) => { + if (p === undefined) { + return 'undefined'; + } else if (p === null) { + return 'null'; + } else { + return p; + } + }), + ); } } @@ -7324,7 +7826,7 @@ class EmptyQueryParamsCmp { recordedParams: Params[] = []; constructor(route: ActivatedRoute) { - route.queryParams.forEach(_ => this.recordedParams.push(_)); + route.queryParams.forEach((_) => this.recordedParams.push(_)); } } @@ -7335,15 +7837,16 @@ class RouteCmp { @Component({ selector: 'link-cmp', - template: - ` ` + template: ` `, }) class RelativeLinkInIfCmp { show: boolean = false; } -@Component( - {selector: 'child', template: '
'}) +@Component({ + selector: 'child', + template: '
', +}) class OutletInNgIf { alwaysTrue = true; } @@ -7353,7 +7856,7 @@ class OutletInNgIf { template: ` ` + `, }) class DummyLinkWithParentCmp { protected exact: boolean; @@ -7374,8 +7877,7 @@ class ComponentRecordingRoutePathAndUrl { } @Component({selector: 'root-cmp', template: ``}) -class RootCmp { -} +class RootCmp {} @Component({selector: 'root-cmp-on-init', template: ``}) class RootCmpWithOnInit { @@ -7388,15 +7890,12 @@ class RootCmpWithOnInit { @Component({ selector: 'root-cmp', - template: - `primary [] right []` + template: `primary [] right []`, }) -class RootCmpWithTwoOutlets { -} +class RootCmpWithTwoOutlets {} @Component({selector: 'root-cmp', template: `main []`}) -class RootCmpWithNamedOutlet { -} +class RootCmpWithNamedOutlet {} @Component({selector: 'throwing-cmp', template: ''}) class ThrowingCmp { @@ -7414,8 +7913,6 @@ class ConditionalThrowingCmp { } } - - function advance(fixture: ComponentFixture, millis?: number): void { tick(millis); fixture.detectChanges(); @@ -7430,9 +7927,7 @@ function createRoot(router: Router, type: Type): ComponentFixture { } @Component({selector: 'lazy', template: 'lazy-loaded'}) -class LazyComponent { -} - +class LazyComponent {} @NgModule({ imports: [CommonModule, ...ROUTER_DIRECTIVES], @@ -7469,8 +7964,6 @@ class LazyComponent { ConditionalThrowingCmp, ], - - declarations: [ BlankCmp, SimpleCmp, @@ -7501,7 +7994,6 @@ class LazyComponent { EmptyQueryParamsCmp, ThrowingCmp, ConditionalThrowingCmp, - ] + ], }) -class TestModule { -} +class TestModule {} diff --git a/packages/router/test/operators/prioritized_guard_value.spec.ts b/packages/router/test/operators/prioritized_guard_value.spec.ts index f87df88d39e..1f97c9f22bd 100644 --- a/packages/router/test/operators/prioritized_guard_value.spec.ts +++ b/packages/router/test/operators/prioritized_guard_value.spec.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ - import {TestBed} from '@angular/core/testing'; import {RouterModule} from '@angular/router'; import {TestScheduler} from 'rxjs/testing'; @@ -14,7 +13,6 @@ import {TestScheduler} from 'rxjs/testing'; import {prioritizedGuardValue} from '../../src/operators/prioritized_guard_value'; import {Router} from '../../src/router'; - describe('prioritizedGuardValue operator', () => { let testScheduler: TestScheduler; let router: Router; @@ -39,11 +37,11 @@ describe('prioritizedGuardValue operator', () => { const expected = ' -------------T--'; - expectObservable(source.pipe(prioritizedGuardValue())) - .toBe( - expected, - TF, - /* an error here maybe */); + expectObservable(source.pipe(prioritizedGuardValue())).toBe( + expected, + TF, + /* an error here maybe */ + ); }); }); @@ -56,11 +54,11 @@ describe('prioritizedGuardValue operator', () => { const expected = ' -------------F--'; - expectObservable(source.pipe(prioritizedGuardValue())) - .toBe( - expected, - TF, - /* an error here maybe */); + expectObservable(source.pipe(prioritizedGuardValue())).toBe( + expected, + TF, + /* an error here maybe */ + ); }); }); @@ -76,11 +74,11 @@ describe('prioritizedGuardValue operator', () => { const expected = ' ------------T---'; - expectObservable(source.pipe(prioritizedGuardValue())) - .toBe( - expected, - TF, - /* an error here maybe */); + expectObservable(source.pipe(prioritizedGuardValue())).toBe( + expected, + TF, + /* an error here maybe */ + ); }); }); @@ -98,11 +96,11 @@ describe('prioritizedGuardValue operator', () => { const expected = ' -------------U---'; - expectObservable(source.pipe(prioritizedGuardValue())) - .toBe( - expected, - urlLookup, - /* an error here maybe */); + expectObservable(source.pipe(prioritizedGuardValue())).toBe( + expected, + urlLookup, + /* an error here maybe */ + ); }); }); @@ -120,11 +118,11 @@ describe('prioritizedGuardValue operator', () => { const expected = ' -------------F---'; - expectObservable(source.pipe(prioritizedGuardValue())) - .toBe( - expected, - TF, - /* an error here maybe */); + expectObservable(source.pipe(prioritizedGuardValue())).toBe( + expected, + TF, + /* an error here maybe */ + ); }); }); @@ -142,11 +140,11 @@ describe('prioritizedGuardValue operator', () => { const expected = ' -------------U----'; - expectObservable(source.pipe(prioritizedGuardValue())) - .toBe( - expected, - urlLookup, - /* an error here maybe */); + expectObservable(source.pipe(prioritizedGuardValue())).toBe( + expected, + urlLookup, + /* an error here maybe */ + ); }); }); @@ -166,11 +164,11 @@ describe('prioritizedGuardValue operator', () => { const expected = ' -------------U---'; - expectObservable(source.pipe(prioritizedGuardValue())) - .toBe( - expected, - urlLookup, - /* an error here maybe */); + expectObservable(source.pipe(prioritizedGuardValue())).toBe( + expected, + urlLookup, + /* an error here maybe */ + ); }); }); @@ -187,11 +185,11 @@ describe('prioritizedGuardValue operator', () => { const expected = ' -------------T---'; - expectObservable(source.pipe(prioritizedGuardValue())) - .toBe( - expected, - resultLookup, - /* an error here maybe */); + expectObservable(source.pipe(prioritizedGuardValue())).toBe( + expected, + resultLookup, + /* an error here maybe */ + ); }); }); @@ -204,17 +202,15 @@ describe('prioritizedGuardValue operator', () => { const expected = ' ---------#'; - expectObservable(source.pipe(prioritizedGuardValue())) - .toBe( - expected, - TF, - /* an error here maybe */); + expectObservable(source.pipe(prioritizedGuardValue())).toBe( + expected, + TF, + /* an error here maybe */ + ); }); }); }); - - function assertDeepEquals(a: any, b: any) { return expect(a).toEqual(b); } diff --git a/packages/router/test/operators/resolve_data.spec.ts b/packages/router/test/operators/resolve_data.spec.ts index 305f9ac82b8..457b9411410 100644 --- a/packages/router/test/operators/resolve_data.spec.ts +++ b/packages/router/test/operators/resolve_data.spec.ts @@ -15,32 +15,35 @@ import {EMPTY, interval, NEVER, of} from 'rxjs'; describe('resolveData operator', () => { it('should take only the first emitted value of every resolver', async () => { TestBed.configureTestingModule({ - providers: [provideRouter([{path: '**', children: [], resolve: {e1: () => interval()}}])] + providers: [provideRouter([{path: '**', children: [], resolve: {e1: () => interval()}}])], }); await RouterTestingHarness.create('/'); expect(TestBed.inject(Router).routerState.root.firstChild?.snapshot.data).toEqual({e1: 0}); }); it('should cancel navigation if a resolver does not complete', async () => { - TestBed.configureTestingModule( - {providers: [provideRouter([{path: '**', children: [], resolve: {e1: () => EMPTY}}])]}); + TestBed.configureTestingModule({ + providers: [provideRouter([{path: '**', children: [], resolve: {e1: () => EMPTY}}])], + }); await RouterTestingHarness.create('/a'); expect(TestBed.inject(Router).url).toEqual('/'); }); it('should cancel navigation if 1 of 2 resolvers does not emit', async () => { TestBed.configureTestingModule({ - providers: - [provideRouter([{path: '**', children: [], resolve: {e0: () => of(0), e1: () => EMPTY}}])] + providers: [ + provideRouter([{path: '**', children: [], resolve: {e0: () => of(0), e1: () => EMPTY}}]), + ], }); await RouterTestingHarness.create('/a'); expect(TestBed.inject(Router).url).toEqual('/'); }); - it('should complete instantly if at least one resolver doesn\'t emit', async () => { + it("should complete instantly if at least one resolver doesn't emit", async () => { TestBed.configureTestingModule({ - providers: - [provideRouter([{path: '**', children: [], resolve: {e0: () => EMPTY, e1: () => NEVER}}])] + providers: [ + provideRouter([{path: '**', children: [], resolve: {e0: () => EMPTY, e1: () => NEVER}}]), + ], }); await RouterTestingHarness.create('/a'); expect(TestBed.inject(Router).url).toEqual('/'); @@ -50,29 +53,35 @@ describe('resolveData operator', () => { let value = 0; let bValue = 0; TestBed.configureTestingModule({ - providers: [provideRouter([ - { - path: 'a', - runGuardsAndResolvers: () => false, - children: [{ - path: '', - resolve: {d0: () => ++value}, - runGuardsAndResolvers: 'always', - children: [], - }], - }, - { - path: 'b', - outlet: 'aux', - runGuardsAndResolvers: () => false, - children: [{ - path: '', - resolve: {d1: () => ++bValue}, - runGuardsAndResolvers: 'always', - children: [], - }] - }, - ])] + providers: [ + provideRouter([ + { + path: 'a', + runGuardsAndResolvers: () => false, + children: [ + { + path: '', + resolve: {d0: () => ++value}, + runGuardsAndResolvers: 'always', + children: [], + }, + ], + }, + { + path: 'b', + outlet: 'aux', + runGuardsAndResolvers: () => false, + children: [ + { + path: '', + resolve: {d1: () => ++bValue}, + runGuardsAndResolvers: 'always', + children: [], + }, + ], + }, + ]), + ], }); const router = TestBed.inject(Router); const harness = await RouterTestingHarness.create('/a(aux:b)'); @@ -87,43 +96,52 @@ describe('resolveData operator', () => { it('should update children inherited data when resolvers run', async () => { let value = 0; TestBed.configureTestingModule({ - providers: [provideRouter([{ - path: 'a', - children: [{path: 'b', children: []}], - resolve: {d0: () => ++value}, - runGuardsAndResolvers: 'always', - }])] + providers: [ + provideRouter([ + { + path: 'a', + children: [{path: 'b', children: []}], + resolve: {d0: () => ++value}, + runGuardsAndResolvers: 'always', + }, + ]), + ], }); const harness = await RouterTestingHarness.create('/a/b'); expect(TestBed.inject(Router).routerState.root.firstChild?.snapshot.data).toEqual({d0: 1}); expect(TestBed.inject(Router).routerState.root.firstChild?.firstChild?.snapshot.data).toEqual({ - d0: 1 + d0: 1, }); await harness.navigateByUrl('/a/b#new'); expect(TestBed.inject(Router).routerState.root.firstChild?.snapshot.data).toEqual({d0: 2}); expect(TestBed.inject(Router).routerState.root.firstChild?.firstChild?.snapshot.data).toEqual({ - d0: 2 + d0: 2, }); }); it('should have correct data when parent resolver runs but data is not inherited', async () => { @Component({template: ''}) - class Empty { - } + class Empty {} TestBed.configureTestingModule({ - providers: [provideRouter([{ - path: 'a', - component: Empty, - data: {parent: 'parent'}, - resolve: {other: () => 'other'}, - children: [{ - path: 'b', - data: {child: 'child'}, - component: Empty, - }] - }])] + providers: [ + provideRouter([ + { + path: 'a', + component: Empty, + data: {parent: 'parent'}, + resolve: {other: () => 'other'}, + children: [ + { + path: 'b', + data: {child: 'child'}, + component: Empty, + }, + ], + }, + ]), + ], }); await RouterTestingHarness.create('/a/b'); const rootSnapshot = TestBed.inject(Router).routerState.root.firstChild!.snapshot; @@ -133,22 +151,27 @@ describe('resolveData operator', () => { it('should have static title when there is a resolver', async () => { @Component({template: ''}) - class Empty { - } + class Empty {} TestBed.configureTestingModule({ - providers: [provideRouter([{ - path: 'a', - title: 'a title', - component: Empty, - resolve: {other: () => 'other'}, - children: [{ - path: 'b', - title: 'b title', - component: Empty, - resolve: {otherb: () => 'other b'}, - }] - }])] + providers: [ + provideRouter([ + { + path: 'a', + title: 'a title', + component: Empty, + resolve: {other: () => 'other'}, + children: [ + { + path: 'b', + title: 'b title', + component: Empty, + resolve: {otherb: () => 'other b'}, + }, + ], + }, + ]), + ], }); await RouterTestingHarness.create('/a/b'); const rootSnapshot = TestBed.inject(Router).routerState.root.firstChild!.snapshot; @@ -158,16 +181,24 @@ describe('resolveData operator', () => { it('should inherit resolved data from parent of parent route', async () => { @Component({template: ''}) - class Empty { - } + class Empty {} TestBed.configureTestingModule({ - providers: [provideRouter([{ - path: 'a', - resolve: {aResolve: () => 'a'}, - children: - [{path: 'b', resolve: {bResolve: () => 'b'}, children: [{path: 'c', component: Empty}]}] - }])] + providers: [ + provideRouter([ + { + path: 'a', + resolve: {aResolve: () => 'a'}, + children: [ + { + path: 'b', + resolve: {bResolve: () => 'b'}, + children: [{path: 'c', component: Empty}], + }, + ], + }, + ]), + ], }); await RouterTestingHarness.create('/a/b/c'); const rootSnapshot = TestBed.inject(Router).routerState.root.firstChild!.snapshot; diff --git a/packages/router/test/page_title_strategy_spec.ts b/packages/router/test/page_title_strategy_spec.ts index 40e9631ae70..195b1e2c693 100644 --- a/packages/router/test/page_title_strategy_spec.ts +++ b/packages/router/test/page_title_strategy_spec.ts @@ -11,7 +11,16 @@ import {provideLocationMocks} from '@angular/common/testing'; import {Component, inject, Inject, Injectable, NgModule} from '@angular/core'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {fakeAsync, TestBed, tick} from '@angular/core/testing'; -import {ActivatedRoute, provideRouter, ResolveFn, Router, RouterModule, RouterStateSnapshot, TitleStrategy, withRouterConfig} from '@angular/router'; +import { + ActivatedRoute, + provideRouter, + ResolveFn, + Router, + RouterModule, + RouterStateSnapshot, + TitleStrategy, + withRouterConfig, +} from '@angular/router'; import {RouterTestingHarness} from '@angular/router/testing'; describe('title strategy', () => { @@ -21,87 +30,85 @@ describe('title strategy', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - TestModule, - ], + imports: [TestModule], providers: [ provideLocationMocks(), provideRouter([], withRouterConfig({paramsInheritanceStrategy: 'always'})), - ] + ], }); router = TestBed.inject(Router); document = TestBed.inject(DOCUMENT); }); it('sets page title from data', fakeAsync(() => { - router.resetConfig([{path: 'home', title: 'My Application', component: BlankCmp}]); - router.navigateByUrl('home'); - tick(); - expect(document.title).toBe('My Application'); - })); + router.resetConfig([{path: 'home', title: 'My Application', component: BlankCmp}]); + router.navigateByUrl('home'); + tick(); + expect(document.title).toBe('My Application'); + })); it('sets page title from resolved data', fakeAsync(() => { - router.resetConfig([{path: 'home', title: TitleResolver, component: BlankCmp}]); - router.navigateByUrl('home'); - tick(); - expect(document.title).toBe('resolved title'); - })); + router.resetConfig([{path: 'home', title: TitleResolver, component: BlankCmp}]); + router.navigateByUrl('home'); + tick(); + expect(document.title).toBe('resolved title'); + })); it('sets page title from resolved data function', fakeAsync(() => { - router.resetConfig([{path: 'home', title: () => 'resolved title', component: BlankCmp}]); - router.navigateByUrl('home'); - tick(); - expect(document.title).toBe('resolved title'); - })); + router.resetConfig([{path: 'home', title: () => 'resolved title', component: BlankCmp}]); + router.navigateByUrl('home'); + tick(); + expect(document.title).toBe('resolved title'); + })); it('sets title with child routes', fakeAsync(() => { - router.resetConfig([{ - path: 'home', - title: 'My Application', - children: [ - {path: '', title: 'child title', component: BlankCmp}, - ] - }]); - router.navigateByUrl('home'); - tick(); - expect(document.title).toBe('child title'); - })); + router.resetConfig([ + { + path: 'home', + title: 'My Application', + children: [{path: '', title: 'child title', component: BlankCmp}], + }, + ]); + router.navigateByUrl('home'); + tick(); + expect(document.title).toBe('child title'); + })); it('sets title with child routes and named outlets', fakeAsync(() => { - router.resetConfig([ - { - path: 'home', - title: 'My Application', - children: [ - {path: '', title: 'child title', component: BlankCmp}, - {path: '', outlet: 'childaux', title: 'child aux title', component: BlankCmp}, - ], - }, - {path: 'compose', component: BlankCmp, outlet: 'aux', title: 'compose'} - ]); - router.navigateByUrl('home(aux:compose)'); - tick(); - expect(document.title).toBe('child title'); - })); + router.resetConfig([ + { + path: 'home', + title: 'My Application', + children: [ + {path: '', title: 'child title', component: BlankCmp}, + {path: '', outlet: 'childaux', title: 'child aux title', component: BlankCmp}, + ], + }, + {path: 'compose', component: BlankCmp, outlet: 'aux', title: 'compose'}, + ]); + router.navigateByUrl('home(aux:compose)'); + tick(); + expect(document.title).toBe('child title'); + })); it('sets page title with inherited params', fakeAsync(() => { - router.resetConfig([ - { - path: 'home', - title: 'My Application', - children: [ - { - path: '', - title: TitleResolver, - component: BlankCmp, - }, - ], - }, - ]); - router.navigateByUrl('home'); - tick(); - expect(document.title).toBe('resolved title'); - })); + router.resetConfig([ + { + path: 'home', + title: 'My Application', + children: [ + { + path: '', + title: TitleResolver, + component: BlankCmp, + }, + ], + }, + ]); + router.navigateByUrl('home'); + tick(); + expect(document.title).toBe('resolved title'); + })); it('can get the title from the ActivatedRouteSnapshot', async () => { router.resetConfig([ @@ -122,16 +129,18 @@ describe('title strategy', () => { title?: string; constructor() { - this.title$.subscribe(v => this.title = v); + this.title$.subscribe((v) => (this.title = v)); } } - const titleResolver: ResolveFn = route => route.queryParams['id']; - router.resetConfig([{ - path: 'home', - title: titleResolver, - component: HomeCmp, - runGuardsAndResolvers: 'paramsOrQueryParamsChange' - }]); + const titleResolver: ResolveFn = (route) => route.queryParams['id']; + router.resetConfig([ + { + path: 'home', + title: titleResolver, + component: HomeCmp, + runGuardsAndResolvers: 'paramsOrQueryParamsChange', + }, + ]); const harness = await RouterTestingHarness.create(); const homeCmp = await harness.navigateByUrl('/home?id=1', HomeCmp); @@ -143,68 +152,60 @@ describe('title strategy', () => { describe('custom strategies', () => { it('overriding the setTitle method', fakeAsync(() => { - @Injectable({providedIn: 'root'}) - class TemplatePageTitleStrategy extends TitleStrategy { - constructor(@Inject(DOCUMENT) private readonly document: Document) { - super(); - } + @Injectable({providedIn: 'root'}) + class TemplatePageTitleStrategy extends TitleStrategy { + constructor(@Inject(DOCUMENT) private readonly document: Document) { + super(); + } - // Example of how setTitle could implement a template for the title - override updateTitle(state: RouterStateSnapshot) { - const title = this.buildTitle(state); - this.document.title = `My Application | ${title}`; - } - } + // Example of how setTitle could implement a template for the title + override updateTitle(state: RouterStateSnapshot) { + const title = this.buildTitle(state); + this.document.title = `My Application | ${title}`; + } + } - TestBed.configureTestingModule({ - imports: [ - TestModule, - ], - providers: [ - provideLocationMocks(), - provideRouter([]), - {provide: TitleStrategy, useClass: TemplatePageTitleStrategy}, - ] - }); - const router = TestBed.inject(Router); - const document = TestBed.inject(DOCUMENT); - router.resetConfig([ - { - path: 'home', - title: 'Home', - children: [ - {path: '', title: 'Child', component: BlankCmp}, - ], - }, - ]); + TestBed.configureTestingModule({ + imports: [TestModule], + providers: [ + provideLocationMocks(), + provideRouter([]), + {provide: TitleStrategy, useClass: TemplatePageTitleStrategy}, + ], + }); + const router = TestBed.inject(Router); + const document = TestBed.inject(DOCUMENT); + router.resetConfig([ + { + path: 'home', + title: 'Home', + children: [{path: '', title: 'Child', component: BlankCmp}], + }, + ]); - router.navigateByUrl('home'); - tick(); - expect(document.title).toEqual('My Application | Child'); - })); + router.navigateByUrl('home'); + tick(); + expect(document.title).toEqual('My Application | Child'); + })); }); }); @Component({template: ''}) -export class BlankCmp { -} +export class BlankCmp {} @Component({ template: ` -` +`, }) -export class RootCmp { -} +export class RootCmp {} @NgModule({ declarations: [BlankCmp], imports: [RouterModule.forRoot([])], }) -export class TestModule { -} - +export class TestModule {} @Injectable({providedIn: 'root'}) export class TitleResolver { diff --git a/packages/router/test/recognize.spec.ts b/packages/router/test/recognize.spec.ts index 0fe301a5414..f4010fc6cb3 100644 --- a/packages/router/test/recognize.spec.ts +++ b/packages/router/test/recognize.spec.ts @@ -24,8 +24,10 @@ describe('recognize', async () => { }); it('should freeze params object', async () => { - const s: RouterStateSnapshot = - await recognize([{path: 'a/:id', component: ComponentA}], 'a/10'); + const s: RouterStateSnapshot = await recognize( + [{path: 'a/:id', component: ComponentA}], + 'a/10', + ); checkActivatedRoute(s.root, '', {}, RootComponent); const child = s.root.firstChild!; expect(Object.isFrozen(child.params)).toBeTruthy(); @@ -33,8 +35,10 @@ describe('recognize', async () => { it('should freeze data object (but not original route data)', async () => { const someData = {a: 1}; - const s: RouterStateSnapshot = - await recognize([{path: '**', component: ComponentA, data: someData}], 'a'); + const s: RouterStateSnapshot = await recognize( + [{path: '**', component: ComponentA, data: someData}], + 'a', + ); checkActivatedRoute(s.root, '', {}, RootComponent); const child = s.root.firstChild!; expect(Object.isFrozen(child.data)).toBeTruthy(); @@ -43,11 +47,13 @@ describe('recognize', async () => { it('should support secondary routes', async () => { const s: RouterStateSnapshot = await recognize( - [ - {path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'}, - {path: 'c', component: ComponentC, outlet: 'right'} - ], - 'a(left:b//right:c)'); + [ + {path: 'a', component: ComponentA}, + {path: 'b', component: ComponentB, outlet: 'left'}, + {path: 'c', component: ComponentC, outlet: 'right'}, + ], + 'a(left:b//right:c)', + ); const c = s.root.children; checkActivatedRoute(c[0], 'a', {}, ComponentA); checkActivatedRoute(c[1], 'b', {}, ComponentB, 'left'); @@ -57,11 +63,13 @@ describe('recognize', async () => { it('should set url segment and index properly', async () => { const url = tree('a(left:b//right:c)'); const s = await recognize( - [ - {path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'}, - {path: 'c', component: ComponentC, outlet: 'right'} - ], - 'a(left:b//right:c)'); + [ + {path: 'a', component: ComponentA}, + {path: 'b', component: ComponentB, outlet: 'left'}, + {path: 'c', component: ComponentC, outlet: 'right'}, + ], + 'a(left:b//right:c)', + ); expect(s.root.url.toString()).toEqual(url.root.toString()); const c = s.root.children; @@ -74,28 +82,36 @@ describe('recognize', async () => { it('should match routes in the depth first order', async () => { const s = await recognize( - [ - {path: 'a', component: ComponentA, children: [{path: ':id', component: ComponentB}]}, - {path: 'a/:id', component: ComponentC} - ], - 'a/paramA'); + [ + {path: 'a', component: ComponentA, children: [{path: ':id', component: ComponentB}]}, + {path: 'a/:id', component: ComponentC}, + ], + 'a/paramA', + ); checkActivatedRoute(s.root, '', {}, RootComponent); checkActivatedRoute(s.root.firstChild!, 'a', {}, ComponentA); checkActivatedRoute(s.root.firstChild!.firstChild!, 'paramA', {id: 'paramA'}, ComponentB); const s2 = await recognize( - [{path: 'a', component: ComponentA}, {path: 'a/:id', component: ComponentC}], 'a/paramA'); + [ + {path: 'a', component: ComponentA}, + {path: 'a/:id', component: ComponentC}, + ], + 'a/paramA', + ); checkActivatedRoute(s2.root, '', {}, RootComponent); checkActivatedRoute(s2.root.firstChild!, 'a/paramA', {id: 'paramA'}, ComponentC); }); it('should use outlet name when matching secondary routes', async () => { const s = await recognize( - [ - {path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'}, - {path: 'b', component: ComponentC, outlet: 'right'} - ], - 'a(right:b)'); + [ + {path: 'a', component: ComponentA}, + {path: 'b', component: ComponentB, outlet: 'left'}, + {path: 'b', component: ComponentC, outlet: 'right'}, + ], + 'a(right:b)', + ); const c = s.root.children; checkActivatedRoute(c[0], 'a', {}, ComponentA); checkActivatedRoute(c[1], 'b', {}, ComponentC, 'right'); @@ -103,16 +119,18 @@ describe('recognize', async () => { it('should handle non top-level secondary routes', async () => { const s = await recognize( - [ - { - path: 'a', - component: ComponentA, - children: [ - {path: 'b', component: ComponentB}, {path: 'c', component: ComponentC, outlet: 'left'} - ] - }, - ], - 'a/(b//left:c)'); + [ + { + path: 'a', + component: ComponentA, + children: [ + {path: 'b', component: ComponentB}, + {path: 'c', component: ComponentC, outlet: 'left'}, + ], + }, + ], + 'a/(b//left:c)', + ); const c = s.root.firstChild!.children; checkActivatedRoute(c[0], 'b', {}, ComponentB, PRIMARY_OUTLET); checkActivatedRoute(c[1], 'c', {}, ComponentC, 'left'); @@ -120,11 +138,13 @@ describe('recognize', async () => { it('should sort routes by outlet name', async () => { const s = await recognize( - [ - {path: 'a', component: ComponentA}, {path: 'c', component: ComponentC, outlet: 'c'}, - {path: 'b', component: ComponentB, outlet: 'b'} - ], - 'a(c:c//b:b)'); + [ + {path: 'a', component: ComponentA}, + {path: 'c', component: ComponentC, outlet: 'c'}, + {path: 'b', component: ComponentB, outlet: 'b'}, + ], + 'a(c:c//b:b)', + ); const c = s.root.children; checkActivatedRoute(c[0], 'a', {}, ComponentA); checkActivatedRoute(c[1], 'b', {}, ComponentB, 'b'); @@ -133,11 +153,12 @@ describe('recognize', async () => { it('should support matrix parameters', async () => { const s = await recognize( - [ - {path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}, - {path: 'c', component: ComponentC, outlet: 'left'} - ], - 'a;a1=11;a2=22/b;b1=111;b2=222(left:c;c1=1111;c2=2222)'); + [ + {path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}, + {path: 'c', component: ComponentC, outlet: 'left'}, + ], + 'a;a1=11;a2=22/b;b1=111;b2=222(left:c;c1=1111;c2=2222)', + ); const c = s.root.children; checkActivatedRoute(c[0], 'a', {a1: '11', a2: '22'}, ComponentA); checkActivatedRoute(c[0].firstChild!, 'b', {b1: '111', b2: '222'}, ComponentB); @@ -151,60 +172,75 @@ describe('recognize', async () => { expect(r.data).toEqual({one: 1}); }); - it('should inherit componentless route\'s data', async () => { + it("should inherit componentless route's data", async () => { const s = await recognize( - [{ + [ + { path: 'a', data: {one: 1}, - children: [{path: 'b', data: {two: 2}, component: ComponentB}] - }], - 'a/b'); + children: [{path: 'b', data: {two: 2}, component: ComponentB}], + }, + ], + 'a/b', + ); const r: ActivatedRouteSnapshot = s.root.firstChild!.firstChild!; expect(r.data).toEqual({one: 1, two: 2}); }); - it('should not inherit route\'s data if it has component', async () => { + it("should not inherit route's data if it has component", async () => { const s = await recognize( - [{ + [ + { path: 'a', component: ComponentA, data: {one: 1}, - children: [{path: 'b', data: {two: 2}, component: ComponentB}] - }], - 'a/b'); + children: [{path: 'b', data: {two: 2}, component: ComponentB}], + }, + ], + 'a/b', + ); const r: ActivatedRouteSnapshot = s.root.firstChild!.firstChild!; expect(r.data).toEqual({two: 2}); }); - it('should not inherit route\'s data if it has loadComponent', async () => { + it("should not inherit route's data if it has loadComponent", async () => { const s = await recognize( - [{ + [ + { path: 'a', loadComponent: () => ComponentA, data: {one: 1}, - children: [{path: 'b', data: {two: 2}, component: ComponentB}] - }], - 'a/b'); + children: [{path: 'b', data: {two: 2}, component: ComponentB}], + }, + ], + 'a/b', + ); const r: ActivatedRouteSnapshot = s.root.firstChild!.firstChild!; expect(r.data).toEqual({two: 2}); }); - it('should inherit route\'s data if paramsInheritanceStrategy is \'always\'', async () => { + it("should inherit route's data if paramsInheritanceStrategy is 'always'", async () => { const s = await recognize( - [{ + [ + { path: 'a', component: ComponentA, data: {one: 1}, - children: [{path: 'b', data: {two: 2}, component: ComponentB}] - }], - 'a/b', 'always'); + children: [{path: 'b', data: {two: 2}, component: ComponentB}], + }, + ], + 'a/b', + 'always', + ); const r: ActivatedRouteSnapshot = s.root.firstChild!.firstChild!; expect(r.data).toEqual({one: 1, two: 2}); }); it('should set resolved data', async () => { - const s = - await recognize([{path: 'a', resolve: {one: 'some-token'}, component: ComponentA}], 'a'); + const s = await recognize( + [{path: 'a', resolve: {one: 'some-token'}, component: ComponentA}], + 'a', + ); const r: any = s.root.firstChild!; expect(r._resolve).toEqual({one: 'some-token'}); }); @@ -224,20 +260,26 @@ describe('recognize', async () => { it('should work (nested case)', async () => { const s = await recognize( - [{path: '', component: ComponentA, children: [{path: '', component: ComponentB}]}], ''); + [{path: '', component: ComponentA, children: [{path: '', component: ComponentB}]}], + '', + ); checkActivatedRoute(s.root.firstChild!, '', {}, ComponentA); checkActivatedRoute(s.root.firstChild!.firstChild!, '', {}, ComponentB); }); it('should inherit params', async () => { const s = await recognize( - [{ + [ + { path: 'a', component: ComponentA, - children: - [{path: '', component: ComponentB, children: [{path: '', component: ComponentC}]}] - }], - '/a;p=1'); + children: [ + {path: '', component: ComponentB, children: [{path: '', component: ComponentC}]}, + ], + }, + ], + '/a;p=1', + ); checkActivatedRoute(s.root.firstChild!, 'a', {p: '1'}, ComponentA); checkActivatedRoute(s.root.firstChild!.firstChild!, '', {p: '1'}, ComponentB); checkActivatedRoute(s.root.firstChild!.firstChild!.firstChild!, '', {p: '1'}, ComponentC); @@ -247,14 +289,18 @@ describe('recognize', async () => { describe('aux split is in the middle', async () => { it('should match (non-terminal)', async () => { const s = await recognize( - [{ + [ + { path: 'a', component: ComponentA, children: [ - {path: 'b', component: ComponentB}, {path: '', component: ComponentC, outlet: 'aux'} - ] - }], - 'a/b'); + {path: 'b', component: ComponentB}, + {path: '', component: ComponentC, outlet: 'aux'}, + ], + }, + ], + 'a/b', + ); checkActivatedRoute(s.root.firstChild!, 'a', {}, ComponentA); const c = s.root.firstChild!.children; @@ -262,49 +308,53 @@ describe('recognize', async () => { checkActivatedRoute(c[1], '', {}, ComponentC, 'aux'); }); - it('should match (non-terminal) when both primary and secondary and primary has a child', - async () => { - const config = [{ - path: 'parent', - children: [ - { - path: '', - component: ComponentA, - children: [ - {path: 'b', component: ComponentB}, - {path: 'c', component: ComponentC}, - ] - }, - { - path: '', - component: ComponentD, - outlet: 'secondary', - } - ] - }]; + it('should match (non-terminal) when both primary and secondary and primary has a child', async () => { + const config = [ + { + path: 'parent', + children: [ + { + path: '', + component: ComponentA, + children: [ + {path: 'b', component: ComponentB}, + {path: 'c', component: ComponentC}, + ], + }, + { + path: '', + component: ComponentD, + outlet: 'secondary', + }, + ], + }, + ]; - const s = await recognize(config, 'parent/b'); - checkActivatedRoute(s.root, '', {}, RootComponent); - checkActivatedRoute(s.root.firstChild!, 'parent', {}, null); + const s = await recognize(config, 'parent/b'); + checkActivatedRoute(s.root, '', {}, RootComponent); + checkActivatedRoute(s.root.firstChild!, 'parent', {}, null); - const cc = s.root.firstChild!.children; - checkActivatedRoute(cc[0], '', {}, ComponentA); - checkActivatedRoute(cc[1], '', {}, ComponentD, 'secondary'); + const cc = s.root.firstChild!.children; + checkActivatedRoute(cc[0], '', {}, ComponentA); + checkActivatedRoute(cc[1], '', {}, ComponentD, 'secondary'); - checkActivatedRoute(cc[0].firstChild!, 'b', {}, ComponentB); - }); + checkActivatedRoute(cc[0].firstChild!, 'b', {}, ComponentB); + }); it('should match (terminal)', async () => { const s = await recognize( - [{ + [ + { path: 'a', component: ComponentA, children: [ {path: 'b', component: ComponentB}, - {path: '', pathMatch: 'full', component: ComponentC, outlet: 'aux'} - ] - }], - 'a/b'); + {path: '', pathMatch: 'full', component: ComponentC, outlet: 'aux'}, + ], + }, + ], + 'a/b', + ); checkActivatedRoute(s.root.firstChild!, 'a', {}, ComponentA); const c = s.root.firstChild!.children; @@ -316,15 +366,18 @@ describe('recognize', async () => { describe('aux split at the end (no right child)', async () => { it('should match (non-terminal)', async () => { const s = await recognize( - [{ + [ + { path: 'a', component: ComponentA, children: [ {path: '', component: ComponentB}, {path: '', component: ComponentC, outlet: 'aux'}, - ] - }], - 'a'); + ], + }, + ], + 'a', + ); checkActivatedRoute(s.root.firstChild!, 'a', {}, ComponentA); const c = s.root.firstChild!.children; @@ -334,15 +387,18 @@ describe('recognize', async () => { it('should match (terminal)', async () => { const s = await recognize( - [{ + [ + { path: 'a', component: ComponentA, children: [ {path: '', pathMatch: 'full', component: ComponentB}, {path: '', pathMatch: 'full', component: ComponentC, outlet: 'aux'}, - ] - }], - 'a'); + ], + }, + ], + 'a', + ); checkActivatedRoute(s.root.firstChild!, 'a', {}, ComponentA); const c = s.root.firstChild!.children; @@ -352,15 +408,18 @@ describe('recognize', async () => { it('should work only only primary outlet', async () => { const s = await recognize( - [{ + [ + { path: 'a', component: ComponentA, children: [ {path: '', component: ComponentB}, {path: 'c', component: ComponentC, outlet: 'aux'}, - ] - }], - 'a/(aux:c)'); + ], + }, + ], + 'a/(aux:c)', + ); checkActivatedRoute(s.root.firstChild!, 'a', {}, ComponentA); const c = s.root.firstChild!.children; @@ -370,11 +429,13 @@ describe('recognize', async () => { it('should work when split is at the root level', async () => { const s = await recognize( - [ - {path: '', component: ComponentA}, {path: 'b', component: ComponentB}, - {path: 'c', component: ComponentC, outlet: 'aux'} - ], - '(aux:c)'); + [ + {path: '', component: ComponentA}, + {path: 'b', component: ComponentB}, + {path: 'c', component: ComponentC, outlet: 'aux'}, + ], + '(aux:c)', + ); checkActivatedRoute(s.root, '', {}, RootComponent); const children = s.root.children; @@ -387,7 +448,8 @@ describe('recognize', async () => { describe('split at the end (right child)', async () => { it('should match (non-terminal)', async () => { const s = await recognize( - [{ + [ + { path: 'a', component: ComponentA, children: [ @@ -396,11 +458,13 @@ describe('recognize', async () => { path: '', component: ComponentC, outlet: 'aux', - children: [{path: 'e', component: ComponentE}] + children: [{path: 'e', component: ComponentE}], }, - ] - }], - 'a/(d//aux:e)'); + ], + }, + ], + 'a/(d//aux:e)', + ); checkActivatedRoute(s.root.firstChild!, 'a', {}, ComponentA); const c = s.root.firstChild!.children; @@ -414,62 +478,71 @@ describe('recognize', async () => { describe('with outlets', async () => { it('should work when outlet is a child of empty path parent', async () => { const s = await recognize( - [{ + [ + { path: '', component: ComponentA, - children: [{path: 'b', outlet: 'b', component: ComponentB}] - }], - '(b:b)'); + children: [{path: 'b', outlet: 'b', component: ComponentB}], + }, + ], + '(b:b)', + ); checkActivatedRoute(s.root.children[0], '', {}, ComponentA); checkActivatedRoute(s.root.children[0].children[0], 'b', {}, ComponentB, 'b'); }); it('should work for outlets adjacent to empty path', async () => { const s = await recognize( - [ - { - path: '', - component: ComponentA, - children: [{path: '', component: ComponentC}], - }, - {path: 'b', outlet: 'b', component: ComponentB} - ], - '(b:b)'); + [ + { + path: '', + component: ComponentA, + children: [{path: '', component: ComponentC}], + }, + {path: 'b', outlet: 'b', component: ComponentB}, + ], + '(b:b)', + ); const [primaryChild, outletChild] = s.root.children; checkActivatedRoute(primaryChild, '', {}, ComponentA); checkActivatedRoute(outletChild, 'b', {}, ComponentB, 'b'); checkActivatedRoute(primaryChild.children[0], '', {}, ComponentC); }); - it('should work with named outlets both adjecent to and as a child of empty path', - async () => { - const s = await recognize( - [ - { - path: '', - component: ComponentA, - children: [{path: 'b', outlet: 'b', component: ComponentB}] - }, - {path: 'c', outlet: 'c', component: ComponentC} - ], - '(b:b//c:c)'); - checkActivatedRoute(s.root.children[0], '', {}, ComponentA); - checkActivatedRoute(s.root.children[1], 'c', {}, ComponentC, 'c'); - checkActivatedRoute(s.root.children[0].children[0], 'b', {}, ComponentB, 'b'); - }); + it('should work with named outlets both adjecent to and as a child of empty path', async () => { + const s = await recognize( + [ + { + path: '', + component: ComponentA, + children: [{path: 'b', outlet: 'b', component: ComponentB}], + }, + {path: 'c', outlet: 'c', component: ComponentC}, + ], + '(b:b//c:c)', + ); + checkActivatedRoute(s.root.children[0], '', {}, ComponentA); + checkActivatedRoute(s.root.children[1], 'c', {}, ComponentC, 'c'); + checkActivatedRoute(s.root.children[0].children[0], 'b', {}, ComponentB, 'b'); + }); it('should work with children outlets within two levels of empty parents', async () => { const s = await recognize( - [{ + [ + { path: '', component: ComponentA, - children: [{ - path: '', - component: ComponentB, - children: [{path: 'c', outlet: 'c', component: ComponentC}], - }] - }], - '(c:c)'); + children: [ + { + path: '', + component: ComponentB, + children: [{path: 'c', outlet: 'c', component: ComponentC}], + }, + ], + }, + ], + '(c:c)', + ); const [compAConfig] = s.root.children; checkActivatedRoute(compAConfig, '', {}, ComponentA); expect(compAConfig.children.length).toBe(1); @@ -482,24 +555,27 @@ describe('recognize', async () => { checkActivatedRoute(compCConfig, 'c', {}, ComponentC, 'c'); }); - it('should not persist a primary segment beyond the boundary of a named outlet match', - async () => { - const recognizePromise = new Recognizer( - TestBed.inject(EnvironmentInjector), - TestBed.inject(RouterConfigLoader), RootComponent, - [ - { - path: '', - component: ComponentA, - outlet: 'a', - children: [{path: 'b', component: ComponentB}], - }, - ], - tree('/b'), 'emptyOnly', new DefaultUrlSerializer()) - .recognize() - .toPromise(); - await expectAsync(recognizePromise).toBeRejected(); - }); + it('should not persist a primary segment beyond the boundary of a named outlet match', async () => { + const recognizePromise = new Recognizer( + TestBed.inject(EnvironmentInjector), + TestBed.inject(RouterConfigLoader), + RootComponent, + [ + { + path: '', + component: ComponentA, + outlet: 'a', + children: [{path: 'b', component: ComponentB}], + }, + ], + tree('/b'), + 'emptyOnly', + new DefaultUrlSerializer(), + ) + .recognize() + .toPromise(); + await expectAsync(recognizePromise).toBeRejected(); + }); }); }); @@ -513,13 +589,17 @@ describe('recognize', async () => { describe('componentless routes', async () => { it('should work', async () => { const s = await recognize( - [{ + [ + { path: 'p/:id', children: [ - {path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'aux'} - ] - }], - 'p/11;pp=22/(a;pa=33//aux:b;pb=44)'); + {path: 'a', component: ComponentA}, + {path: 'b', component: ComponentB, outlet: 'aux'}, + ], + }, + ], + 'p/11;pp=22/(a;pa=33//aux:b;pb=44)', + ); const p = s.root.firstChild!; checkActivatedRoute(p, 'p/11', {id: '11', pp: '22'}, null); @@ -530,16 +610,25 @@ describe('recognize', async () => { it('should inherit params until encounters a normal route', async () => { const s = await recognize( - [{ + [ + { path: 'p/:id', - children: [{ - path: 'a/:name', - children: [ - {path: 'b', component: ComponentB, children: [{path: 'c', component: ComponentC}]} - ] - }] - }], - 'p/11/a/victor/b/c'); + children: [ + { + path: 'a/:name', + children: [ + { + path: 'b', + component: ComponentB, + children: [{path: 'c', component: ComponentC}], + }, + ], + }, + ], + }, + ], + 'p/11/a/victor/b/c', + ); const p = s.root.firstChild!; checkActivatedRoute(p, 'p/11', {id: '11'}, null); @@ -553,18 +642,28 @@ describe('recognize', async () => { checkActivatedRoute(c, 'c', {}, ComponentC); }); - it('should inherit all params if paramsInheritanceStrategy is \'always\'', async () => { + it("should inherit all params if paramsInheritanceStrategy is 'always'", async () => { const s = await recognize( - [{ + [ + { path: 'p/:id', - children: [{ - path: 'a/:name', - children: [ - {path: 'b', component: ComponentB, children: [{path: 'c', component: ComponentC}]} - ] - }] - }], - 'p/11/a/victor/b/c', 'always'); + children: [ + { + path: 'a/:name', + children: [ + { + path: 'b', + component: ComponentB, + children: [{path: 'c', component: ComponentC}], + }, + ], + }, + ], + }, + ], + 'p/11/a/victor/b/c', + 'always', + ); const c = s.root.firstChild!.firstChild!.firstChild!.firstChild!; checkActivatedRoute(c, 'c', {id: '11', name: 'victor'}, ComponentC); }); @@ -573,23 +672,27 @@ describe('recognize', async () => { describe('empty URL leftovers', async () => { it('should not throw when no children matching', async () => { const s = await recognize( - [{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}], - '/a'); + [{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}], + '/a', + ); const a = s.root.firstChild; checkActivatedRoute(a!, 'a', {}, ComponentA); }); it('should not throw when no children matching (aux routes)', async () => { const s = await recognize( - [{ + [ + { path: 'a', component: ComponentA, children: [ {path: 'b', component: ComponentB}, {path: '', component: ComponentC, outlet: 'aux'}, - ] - }], - '/a'); + ], + }, + ], + '/a', + ); const a = s.root.firstChild!; checkActivatedRoute(a, 'a', {}, ComponentA); checkActivatedRoute(a.children[0], '', {}, ComponentC, 'aux'); @@ -607,19 +710,22 @@ describe('recognize', async () => { }; const s = await recognize( - [{ + [ + { matcher: matcher, component: ComponentA, - children: [{path: 'b', component: ComponentB}] - }] as any, - '/a/1;p=99/b'); + children: [{path: 'b', component: ComponentB}], + }, + ] as any, + '/a/1;p=99/b', + ); const a = s.root.firstChild!; checkActivatedRoute(a, 'a/1', {id: '1', p: '99'}, ComponentA); checkActivatedRoute(a.firstChild!, 'b', {}, ComponentB); }); it('should work with terminal route', async () => { - const matcher = (s: any, g: any, r: any) => s.length === 0 ? ({consumed: s}) : null; + const matcher = (s: any, g: any, r: any) => (s.length === 0 ? {consumed: s} : null); const s = await recognize([{matcher, component: ComponentA}], ''); const a = s.root.firstChild!; @@ -627,10 +733,12 @@ describe('recognize', async () => { }); it('should work with child terminal route', async () => { - const matcher = (s: any, g: any, r: any) => s.length === 0 ? ({consumed: s}) : null; + const matcher = (s: any, g: any, r: any) => (s.length === 0 ? {consumed: s} : null); const s = await recognize( - [{path: 'a', component: ComponentA, children: [{matcher, component: ComponentB}]}], 'a'); + [{path: 'a', component: ComponentA, children: [{matcher, component: ComponentB}]}], + 'a', + ); const a = s.root.firstChild!; checkActivatedRoute(a, 'a', {}, ComponentA); }); @@ -668,7 +776,7 @@ describe('recognize', async () => { it('should run canMatch guards on wildcard routes', async () => { const config = [ {path: '**', component: ComponentA, data: {id: 'a'}, canMatch: [() => false]}, - {path: '**', component: ComponentB, data: {id: 'b'}} + {path: '**', component: ComponentB, data: {id: 'b'}}, ]; const s = await recognize(config, 'a'); expect(s.root.firstChild!.data['id']).toEqual('b'); @@ -677,24 +785,36 @@ describe('recognize', async () => { }); async function recognize( - config: Routes, url: string, - paramsInheritanceStrategy: 'emptyOnly'|'always' = 'emptyOnly'): Promise { + config: Routes, + url: string, + paramsInheritanceStrategy: 'emptyOnly' | 'always' = 'emptyOnly', +): Promise { const serializer = new DefaultUrlSerializer(); const result = await new Recognizer( - TestBed.inject(EnvironmentInjector), TestBed.inject(RouterConfigLoader), - RootComponent, config, tree(url), paramsInheritanceStrategy, serializer) - .recognize() - .toPromise(); + TestBed.inject(EnvironmentInjector), + TestBed.inject(RouterConfigLoader), + RootComponent, + config, + tree(url), + paramsInheritanceStrategy, + serializer, + ) + .recognize() + .toPromise(); return result!.state; } function checkActivatedRoute( - actual: ActivatedRouteSnapshot, url: string, params: Params, cmp: Function|null, - outlet: string = PRIMARY_OUTLET): void { + actual: ActivatedRouteSnapshot, + url: string, + params: Params, + cmp: Function | null, + outlet: string = PRIMARY_OUTLET, +): void { if (actual === null) { expect(actual).not.toBeNull(); } else { - expect(actual.url.map(s => s.path).join('/')).toEqual(url); + expect(actual.url.map((s) => s.path).join('/')).toEqual(url); expect(actual.params).toEqual(params); expect(actual.component).toBe(cmp); expect(actual.outlet).toEqual(outlet); diff --git a/packages/router/test/regression_integration.spec.ts b/packages/router/test/regression_integration.spec.ts index 585a345ab4a..ba145cc4ffb 100644 --- a/packages/router/test/regression_integration.spec.ts +++ b/packages/router/test/regression_integration.spec.ts @@ -8,9 +8,27 @@ import {CommonModule, HashLocationStrategy, Location, LocationStrategy} from '@angular/common'; import {provideLocationMocks, SpyLocation} from '@angular/common/testing'; -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Injectable, NgModule, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Injectable, + NgModule, + TemplateRef, + Type, + ViewChild, + ViewContainerRef, +} from '@angular/core'; import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; -import {ChildrenOutletContexts, DefaultUrlSerializer, Router, RouterModule, RouterOutlet, UrlSerializer, UrlTree} from '@angular/router'; +import { + ChildrenOutletContexts, + DefaultUrlSerializer, + Router, + RouterModule, + RouterOutlet, + UrlSerializer, + UrlTree, +} from '@angular/router'; import {of} from 'rxjs'; import {delay, mapTo} from 'rxjs/operators'; @@ -19,97 +37,98 @@ import {provideRouter} from '../src/provide_router'; describe('Integration', () => { describe('routerLinkActive', () => { it('should update when the associated routerLinks change - #18469', fakeAsync(() => { - @Component({ - template: ` + @Component({ + template: ` {{firstLink}} `, - }) - class LinkComponent { - firstLink = 'link-a'; - secondLink = 'link-b'; + }) + class LinkComponent { + firstLink = 'link-a'; + secondLink = 'link-b'; - changeLinks(): void { - const temp = this.secondLink; - this.secondLink = this.firstLink; - this.firstLink = temp; - } - } + changeLinks(): void { + const temp = this.secondLink; + this.secondLink = this.firstLink; + this.firstLink = temp; + } + } - @Component({template: 'simple'}) - class SimpleCmp { - } + @Component({template: 'simple'}) + class SimpleCmp {} - TestBed.configureTestingModule({ - imports: [RouterModule.forRoot( - [{path: 'link-a', component: SimpleCmp}, {path: 'link-b', component: SimpleCmp}])], - declarations: [LinkComponent, SimpleCmp] - }); + TestBed.configureTestingModule({ + imports: [ + RouterModule.forRoot([ + {path: 'link-a', component: SimpleCmp}, + {path: 'link-b', component: SimpleCmp}, + ]), + ], + declarations: [LinkComponent, SimpleCmp], + }); - const router: Router = TestBed.inject(Router); - const fixture = createRoot(router, LinkComponent); - const firstLink = fixture.debugElement.query(p => p.nativeElement.id === 'first-link'); - const secondLink = fixture.debugElement.query(p => p.nativeElement.id === 'second-link'); - router.navigateByUrl('/link-a'); - advance(fixture); + const router: Router = TestBed.inject(Router); + const fixture = createRoot(router, LinkComponent); + const firstLink = fixture.debugElement.query((p) => p.nativeElement.id === 'first-link'); + const secondLink = fixture.debugElement.query((p) => p.nativeElement.id === 'second-link'); + router.navigateByUrl('/link-a'); + advance(fixture); - expect(firstLink.nativeElement.classList).toContain('active'); - expect(secondLink.nativeElement.classList).not.toContain('active'); + expect(firstLink.nativeElement.classList).toContain('active'); + expect(secondLink.nativeElement.classList).not.toContain('active'); - fixture.componentInstance.changeLinks(); - fixture.detectChanges(); - advance(fixture); + fixture.componentInstance.changeLinks(); + fixture.detectChanges(); + advance(fixture); - expect(firstLink.nativeElement.classList).not.toContain('active'); - expect(secondLink.nativeElement.classList).toContain('active'); - })); + expect(firstLink.nativeElement.classList).not.toContain('active'); + expect(secondLink.nativeElement.classList).toContain('active'); + })); it('should not cause infinite loops in the change detection - #15825', fakeAsync(() => { - @Component({selector: 'simple', template: 'simple'}) - class SimpleCmp { - } + @Component({selector: 'simple', template: 'simple'}) + class SimpleCmp {} - @Component({ - selector: 'some-root', - template: ` + @Component({ + selector: 'some-root', + template: `
- ` - }) - class MyCmp { - show: boolean = false; - } + `, + }) + class MyCmp { + show: boolean = false; + } - @NgModule({ - imports: [CommonModule, RouterModule.forRoot([])], - declarations: [MyCmp, SimpleCmp], - }) - class MyModule { - } + @NgModule({ + imports: [CommonModule, RouterModule.forRoot([])], + declarations: [MyCmp, SimpleCmp], + }) + class MyModule {} - TestBed.configureTestingModule({imports: [MyModule]}); + TestBed.configureTestingModule({imports: [MyModule]}); - const router: Router = TestBed.inject(Router); - const fixture = createRoot(router, MyCmp); - router.resetConfig([{path: 'simple', component: SimpleCmp}]); + const router: Router = TestBed.inject(Router); + const fixture = createRoot(router, MyCmp); + router.resetConfig([{path: 'simple', component: SimpleCmp}]); - router.navigateByUrl('/simple'); - advance(fixture); + router.navigateByUrl('/simple'); + advance(fixture); - const instance = fixture.componentInstance; - instance.show = true; - expect(() => advance(fixture)).not.toThrow(); - })); + const instance = fixture.componentInstance; + instance.show = true; + expect(() => advance(fixture)).not.toThrow(); + })); it('should set isActive right after looking at its children -- #18983', fakeAsync(() => { - @Component({ - template: ` + @Component({ + template: `
isActive: {{rla.isActive}} @@ -119,162 +138,155 @@ describe('Integration', () => {
- ` - }) - class ComponentWithRouterLink { - @ViewChild(TemplateRef, {static: true}) templateRef?: TemplateRef; - @ViewChild('container', {read: ViewContainerRef, static: true}) - container?: ViewContainerRef; + `, + }) + class ComponentWithRouterLink { + @ViewChild(TemplateRef, {static: true}) templateRef?: TemplateRef; + @ViewChild('container', {read: ViewContainerRef, static: true}) + container?: ViewContainerRef; - addLink() { - if (this.templateRef) { - this.container?.createEmbeddedView(this.templateRef, {$implicit: '/simple'}); - } - } + addLink() { + if (this.templateRef) { + this.container?.createEmbeddedView(this.templateRef, {$implicit: '/simple'}); + } + } - removeLink() { - this.container?.clear(); - } - } + removeLink() { + this.container?.clear(); + } + } - @Component({template: 'simple'}) - class SimpleCmp { - } + @Component({template: 'simple'}) + class SimpleCmp {} - TestBed.configureTestingModule({ - imports: [RouterModule.forRoot([{path: 'simple', component: SimpleCmp}])], - declarations: [ComponentWithRouterLink, SimpleCmp] - }); + TestBed.configureTestingModule({ + imports: [RouterModule.forRoot([{path: 'simple', component: SimpleCmp}])], + declarations: [ComponentWithRouterLink, SimpleCmp], + }); - const router: Router = TestBed.inject(Router); - const fixture = createRoot(router, ComponentWithRouterLink); - router.navigateByUrl('/simple'); - advance(fixture); + const router: Router = TestBed.inject(Router); + const fixture = createRoot(router, ComponentWithRouterLink); + router.navigateByUrl('/simple'); + advance(fixture); - fixture.componentInstance.addLink(); - fixture.detectChanges(); + fixture.componentInstance.addLink(); + fixture.detectChanges(); - fixture.componentInstance.removeLink(); - advance(fixture); - advance(fixture); + fixture.componentInstance.removeLink(); + advance(fixture); + advance(fixture); - expect(fixture.nativeElement.innerHTML).toContain('isActive: false'); - })); + expect(fixture.nativeElement.innerHTML).toContain('isActive: false'); + })); it('should set isActive with OnPush change detection - #19934', fakeAsync(() => { - @Component({ - template: ` + @Component({ + template: `
isActive: {{rla.isActive}}
`, - changeDetection: ChangeDetectionStrategy.OnPush - }) - class OnPushComponent { - } + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class OnPushComponent {} - @Component({template: 'simple'}) - class SimpleCmp { - } + @Component({template: 'simple'}) + class SimpleCmp {} - TestBed.configureTestingModule({ - imports: [RouterModule.forRoot([{path: 'simple', component: SimpleCmp}])], - declarations: [OnPushComponent, SimpleCmp] - }); + TestBed.configureTestingModule({ + imports: [RouterModule.forRoot([{path: 'simple', component: SimpleCmp}])], + declarations: [OnPushComponent, SimpleCmp], + }); - const router: Router = TestBed.get(Router); - const fixture = createRoot(router, OnPushComponent); - router.navigateByUrl('/simple'); - advance(fixture); + const router: Router = TestBed.get(Router); + const fixture = createRoot(router, OnPushComponent); + router.navigateByUrl('/simple'); + advance(fixture); - expect(fixture.nativeElement.innerHTML).toContain('isActive: true'); - })); + expect(fixture.nativeElement.innerHTML).toContain('isActive: true'); + })); }); - it('should not reactivate a deactivated outlet when destroyed and recreated - #41379', - fakeAsync(() => { - @Component({template: 'simple'}) - class SimpleComponent { - } + it('should not reactivate a deactivated outlet when destroyed and recreated - #41379', fakeAsync(() => { + @Component({template: 'simple'}) + class SimpleComponent {} - @Component({template: ` `}) - class AppComponent { - outletVisible = true; - } + @Component({template: ` `}) + class AppComponent { + outletVisible = true; + } - TestBed.configureTestingModule({ - imports: - [RouterModule.forRoot([{path: ':id', component: SimpleComponent, outlet: 'aux'}])], - declarations: [SimpleComponent, AppComponent], - }); + TestBed.configureTestingModule({ + imports: [RouterModule.forRoot([{path: ':id', component: SimpleComponent, outlet: 'aux'}])], + declarations: [SimpleComponent, AppComponent], + }); - const router = TestBed.inject(Router); - const fixture = createRoot(router, AppComponent); - const componentCdr = fixture.componentRef.injector.get(ChangeDetectorRef); + const router = TestBed.inject(Router); + const fixture = createRoot(router, AppComponent); + const componentCdr = fixture.componentRef.injector.get(ChangeDetectorRef); - router.navigate([{outlets: {aux: ['1234']}}]); - advance(fixture); - expect(fixture.nativeElement.innerHTML).toContain('simple'); + router.navigate([{outlets: {aux: ['1234']}}]); + advance(fixture); + expect(fixture.nativeElement.innerHTML).toContain('simple'); - router.navigate([{outlets: {aux: null}}]); - advance(fixture); - expect(fixture.nativeElement.innerHTML).not.toContain('simple'); + router.navigate([{outlets: {aux: null}}]); + advance(fixture); + expect(fixture.nativeElement.innerHTML).not.toContain('simple'); - fixture.componentInstance.outletVisible = false; - componentCdr.detectChanges(); - expect(fixture.nativeElement.innerHTML).not.toContain('simple'); - expect(fixture.nativeElement.innerHTML).not.toContain('router-outlet'); + fixture.componentInstance.outletVisible = false; + componentCdr.detectChanges(); + expect(fixture.nativeElement.innerHTML).not.toContain('simple'); + expect(fixture.nativeElement.innerHTML).not.toContain('router-outlet'); - fixture.componentInstance.outletVisible = true; - componentCdr.detectChanges(); - expect(fixture.nativeElement.innerHTML).toContain('router-outlet'); - expect(fixture.nativeElement.innerHTML).not.toContain('simple'); - })); + fixture.componentInstance.outletVisible = true; + componentCdr.detectChanges(); + expect(fixture.nativeElement.innerHTML).toContain('router-outlet'); + expect(fixture.nativeElement.innerHTML).not.toContain('simple'); + })); describe('useHash', () => { it('should restore hash to match current route - #28561', fakeAsync(() => { - @Component({selector: 'root-cmp', template: ``}) - class RootCmp { - } + @Component({selector: 'root-cmp', template: ``}) + class RootCmp {} - @Component({template: 'simple'}) - class SimpleCmp { - } - @Component({template: 'one'}) - class OneCmp { - } + @Component({template: 'simple'}) + class SimpleCmp {} + @Component({template: 'one'}) + class OneCmp {} - TestBed.configureTestingModule({ - imports: [RouterModule.forRoot([ - {path: '', component: SimpleCmp}, - {path: 'one', component: OneCmp, canActivate: ['returnRootUrlTree']} - ])], - declarations: [SimpleCmp, RootCmp, OneCmp], - providers: [ - provideLocationMocks(), - { - provide: 'returnRootUrlTree', - useFactory: (router: Router) => () => { - return router.parseUrl('/'); - }, - deps: [Router] - }, - ], - }); + TestBed.configureTestingModule({ + imports: [ + RouterModule.forRoot([ + {path: '', component: SimpleCmp}, + {path: 'one', component: OneCmp, canActivate: ['returnRootUrlTree']}, + ]), + ], + declarations: [SimpleCmp, RootCmp, OneCmp], + providers: [ + provideLocationMocks(), + { + provide: 'returnRootUrlTree', + useFactory: (router: Router) => () => { + return router.parseUrl('/'); + }, + deps: [Router], + }, + ], + }); - const router = TestBed.inject(Router); - const location = TestBed.inject(Location) as SpyLocation; + const router = TestBed.inject(Router); + const location = TestBed.inject(Location) as SpyLocation; - router.navigateByUrl('/'); - // Will setup location change listeners - const fixture = createRoot(router, RootCmp); + router.navigateByUrl('/'); + // Will setup location change listeners + const fixture = createRoot(router, RootCmp); - location.simulateHashChange('/one'); - advance(fixture); + location.simulateHashChange('/one'); + advance(fixture); - expect(location.path()).toEqual('/'); - expect(fixture.nativeElement.innerHTML).toContain('one'); - })); + expect(location.path()).toEqual('/'); + expect(fixture.nativeElement.innerHTML).toContain('one'); + })); }); describe('duplicate navigation handling (#43447, #43446)', () => { @@ -290,27 +302,22 @@ describe('Integration', () => { } } @Component({selector: 'root-cmp', template: ``}) - class RootCmp { - } + class RootCmp {} @Component({template: 'simple'}) - class SimpleCmp { - } + class SimpleCmp {} @Component({template: 'one'}) - class OneCmp { - } + class OneCmp {} TestBed.configureTestingModule({ declarations: [SimpleCmp, RootCmp, OneCmp], imports: [RouterOutlet], providers: [ DelayedResolve, provideLocationMocks(), - provideRouter( - [ - {path: '', component: SimpleCmp}, - {path: 'one', component: OneCmp, resolve: {x: DelayedResolve}} - ], - ), + provideRouter([ + {path: '', component: SimpleCmp}, + {path: 'one', component: OneCmp, resolve: {x: DelayedResolve}}, + ]), {provide: LocationStrategy, useClass: HashLocationStrategy}, ], }); @@ -324,27 +331,26 @@ describe('Integration', () => { })); it('duplicate navigation to same url', fakeAsync(() => { - location.go('/one'); - tick(100); - location.go('/one'); - tick(1000); - advance(fixture); + location.go('/one'); + tick(100); + location.go('/one'); + tick(1000); + advance(fixture); - expect(location.path()).toEqual('/one'); - expect(fixture.nativeElement.innerHTML).toContain('one'); - })); + expect(location.path()).toEqual('/one'); + expect(fixture.nativeElement.innerHTML).toContain('one'); + })); - it('works with a duplicate popstate/hashchange navigation (as seen in firefox)', - fakeAsync(() => { - (location as any)._subject.emit({'url': 'one', 'pop': true, 'type': 'popstate'}); - tick(1); - (location as any)._subject.emit({'url': 'one', 'pop': true, 'type': 'hashchange'}); - tick(1000); - advance(fixture); + it('works with a duplicate popstate/hashchange navigation (as seen in firefox)', fakeAsync(() => { + (location as any)._subject.emit({'url': 'one', 'pop': true, 'type': 'popstate'}); + tick(1); + (location as any)._subject.emit({'url': 'one', 'pop': true, 'type': 'hashchange'}); + tick(1000); + advance(fixture); - expect(router.routerState.toString()).toContain(`url:'one'`); - expect(fixture.nativeElement.innerHTML).toContain('one'); - })); + expect(router.routerState.toString()).toContain(`url:'one'`); + expect(fixture.nativeElement.innerHTML).toContain('one'); + })); }); it('should not unregister outlet if a different one already exists #36711, 32453', async () => { @@ -360,12 +366,11 @@ describe('Integration', () => { } @Component({template: ''}) - class EmptyCmp { - } + class EmptyCmp {} TestBed.configureTestingModule({ imports: [CommonModule, RouterModule.forRoot([{path: '**', component: EmptyCmp}])], - declarations: [TestCmp, EmptyCmp] + declarations: [TestCmp, EmptyCmp], }); const fixture = TestBed.createComponent(TestCmp); const contexts = TestBed.inject(ChildrenOutletContexts); @@ -403,7 +408,7 @@ describe('Integration', () => { } TestBed.configureTestingModule({ - providers: [provideRouter([]), {provide: UrlSerializer, useValue: new CustomSerializer()}] + providers: [provideRouter([]), {provide: UrlSerializer, useValue: new CustomSerializer()}], }); const router = TestBed.inject(Router); diff --git a/packages/router/test/router.spec.ts b/packages/router/test/router.spec.ts index 9188ae59814..ebe57eb3881 100644 --- a/packages/router/test/router.spec.ts +++ b/packages/router/test/router.spec.ts @@ -36,11 +36,16 @@ describe('Router', () => { it('should copy config to avoid mutations of user-provided objects', () => { const r: Router = TestBed.inject(Router); - const configs: Routes = [{ - path: 'a', - component: TestComponent, - children: [{path: 'b', component: TestComponent}, {path: 'c', component: TestComponent}] - }]; + const configs: Routes = [ + { + path: 'a', + component: TestComponent, + children: [ + {path: 'b', component: TestComponent}, + {path: 'c', component: TestComponent}, + ], + }, + ]; const children = configs[0].children!; r.resetConfig(configs); @@ -86,19 +91,19 @@ describe('Router', () => { }); it('should be idempotent', inject([Router, Location], (r: Router, location: Location) => { - r.setUpLocationChangeListener(); - const a = (r).nonRouterCurrentEntryChangeSubscription; - r.setUpLocationChangeListener(); - const b = (r).nonRouterCurrentEntryChangeSubscription; + r.setUpLocationChangeListener(); + const a = (r).nonRouterCurrentEntryChangeSubscription; + r.setUpLocationChangeListener(); + const b = (r).nonRouterCurrentEntryChangeSubscription; - expect(a).toBe(b); + expect(a).toBe(b); - r.dispose(); - r.setUpLocationChangeListener(); - const c = (r).nonRouterCurrentEntryChangeSubscription; + r.dispose(); + r.setUpLocationChangeListener(); + const c = (r).nonRouterCurrentEntryChangeSubscription; - expect(c).not.toBe(b); - })); + expect(c).not.toBe(b); + })); }); describe('PreActivation', () => { @@ -127,20 +132,32 @@ describe('Router', () => { TestBed.configureTestingModule({ imports: [RouterModule], providers: [ - Logger, provideTokenLogger(CA_CHILD), provideTokenLogger(CA_CHILD_FALSE, false), + Logger, + provideTokenLogger(CA_CHILD), + provideTokenLogger(CA_CHILD_FALSE, false), provideTokenLogger(CA_CHILD_REDIRECT, serializer.parse('/canActivate_child_redirect')), - provideTokenLogger(CAC_CHILD), provideTokenLogger(CAC_CHILD_FALSE, false), + provideTokenLogger(CAC_CHILD), + provideTokenLogger(CAC_CHILD_FALSE, false), provideTokenLogger( - CAC_CHILD_REDIRECT, serializer.parse('/canActivateChild_child_redirect')), - provideTokenLogger(CA_GRANDCHILD), provideTokenLogger(CA_GRANDCHILD_FALSE, false), + CAC_CHILD_REDIRECT, + serializer.parse('/canActivateChild_child_redirect'), + ), + provideTokenLogger(CA_GRANDCHILD), + provideTokenLogger(CA_GRANDCHILD_FALSE, false), provideTokenLogger( - CA_GRANDCHILD_REDIRECT, serializer.parse('/canActivate_grandchild_redirect')), - provideTokenLogger(CDA_CHILD), provideTokenLogger(CDA_CHILD_FALSE, false), + CA_GRANDCHILD_REDIRECT, + serializer.parse('/canActivate_grandchild_redirect'), + ), + provideTokenLogger(CDA_CHILD), + provideTokenLogger(CDA_CHILD_FALSE, false), provideTokenLogger(CDA_CHILD_REDIRECT, serializer.parse('/canDeactivate_child_redirect')), - provideTokenLogger(CDA_GRANDCHILD), provideTokenLogger(CDA_GRANDCHILD_FALSE, false), + provideTokenLogger(CDA_GRANDCHILD), + provideTokenLogger(CDA_GRANDCHILD_FALSE, false), provideTokenLogger( - CDA_GRANDCHILD_REDIRECT, serializer.parse('/canDeactivate_grandchild_redirect')) - ] + CDA_GRANDCHILD_REDIRECT, + serializer.parse('/canDeactivate_grandchild_redirect'), + ), + ], }); }); @@ -158,25 +175,32 @@ describe('Router', () => { * child */ let result = false; - const childSnapshot = - createActivatedRouteSnapshot({component: 'child', routeConfig: {path: 'child'}}); + const childSnapshot = createActivatedRouteSnapshot({ + component: 'child', + routeConfig: {path: 'child'}, + }); const futureState = new (RouterStateSnapshot as any)( - 'url', new TreeNode(empty.root, [new TreeNode(childSnapshot, [])])); + 'url', + new TreeNode(empty.root, [new TreeNode(childSnapshot, [])]), + ); // Since we only test the guards, we don't need to provide a full navigation // transition object with all properties set. const testTransition = { - guards: getAllRouteGuards(futureState, empty, new ChildrenOutletContexts()) + guards: getAllRouteGuards(futureState, empty, new ChildrenOutletContexts()), } as NavigationTransition; of(testTransition) - .pipe(checkGuardsOperator( - TestBed.inject(EnvironmentInjector), - (evt) => { - events.push(evt); - })) - .subscribe((x) => result = !!x.guardsResult, (e) => { + .pipe( + checkGuardsOperator(TestBed.inject(EnvironmentInjector), (evt) => { + events.push(evt); + }), + ) + .subscribe( + (x) => (result = !!x.guardsResult), + (e) => { throw e; - }); + }, + ); expect(result).toBe(true); expect(events.length).toEqual(2); @@ -195,33 +219,44 @@ describe('Router', () => { * great grandchild */ let result = false; - const childSnapshot = - createActivatedRouteSnapshot({component: 'child', routeConfig: {path: 'child'}}); - const grandchildSnapshot = createActivatedRouteSnapshot( - {component: 'grandchild', routeConfig: {path: 'grandchild'}}); - const greatGrandchildSnapshot = createActivatedRouteSnapshot( - {component: 'great-grandchild', routeConfig: {path: 'great-grandchild'}}); + const childSnapshot = createActivatedRouteSnapshot({ + component: 'child', + routeConfig: {path: 'child'}, + }); + const grandchildSnapshot = createActivatedRouteSnapshot({ + component: 'grandchild', + routeConfig: {path: 'grandchild'}, + }); + const greatGrandchildSnapshot = createActivatedRouteSnapshot({ + component: 'great-grandchild', + routeConfig: {path: 'great-grandchild'}, + }); const futureState = new (RouterStateSnapshot as any)( - 'url', - new TreeNode( - empty.root, [new TreeNode(childSnapshot, [ - new TreeNode(grandchildSnapshot, [new TreeNode(greatGrandchildSnapshot, [])]) - ])])); + 'url', + new TreeNode(empty.root, [ + new TreeNode(childSnapshot, [ + new TreeNode(grandchildSnapshot, [new TreeNode(greatGrandchildSnapshot, [])]), + ]), + ]), + ); // Since we only test the guards, we don't need to provide a full navigation // transition object with all properties set. const testTransition = { - guards: getAllRouteGuards(futureState, empty, new ChildrenOutletContexts()) + guards: getAllRouteGuards(futureState, empty, new ChildrenOutletContexts()), } as NavigationTransition; of(testTransition) - .pipe(checkGuardsOperator( - TestBed.inject(EnvironmentInjector), - (evt) => { - events.push(evt); - })) - .subscribe((x) => result = !!x.guardsResult, (e) => { + .pipe( + checkGuardsOperator(TestBed.inject(EnvironmentInjector), (evt) => { + events.push(evt); + }), + ) + .subscribe( + (x) => (result = !!x.guardsResult), + (e) => { throw e; - }); + }, + ); expect(result).toBe(true); expect(events.length).toEqual(6); @@ -240,31 +275,42 @@ describe('Router', () => { * grandchild */ let result = false; - const childSnapshot = - createActivatedRouteSnapshot({component: 'child', routeConfig: {path: 'child'}}); - const grandchildSnapshot = createActivatedRouteSnapshot( - {component: 'grandchild', routeConfig: {path: 'grandchild'}}); + const childSnapshot = createActivatedRouteSnapshot({ + component: 'child', + routeConfig: {path: 'child'}, + }); + const grandchildSnapshot = createActivatedRouteSnapshot({ + component: 'grandchild', + routeConfig: {path: 'grandchild'}, + }); const currentState = new (RouterStateSnapshot as any)( - 'url', new TreeNode(empty.root, [new TreeNode(childSnapshot, [])])); + 'url', + new TreeNode(empty.root, [new TreeNode(childSnapshot, [])]), + ); const futureState = new (RouterStateSnapshot as any)( - 'url', - new TreeNode( - empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])])); + 'url', + new TreeNode(empty.root, [ + new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]), + ]), + ); // Since we only test the guards, we don't need to provide a full navigation // transition object with all properties set. const testTransition = { - guards: getAllRouteGuards(futureState, currentState, new ChildrenOutletContexts()) + guards: getAllRouteGuards(futureState, currentState, new ChildrenOutletContexts()), } as NavigationTransition; of(testTransition) - .pipe(checkGuardsOperator( - TestBed.inject(EnvironmentInjector), - (evt) => { - events.push(evt); - })) - .subscribe((x) => result = !!x.guardsResult, (e) => { + .pipe( + checkGuardsOperator(TestBed.inject(EnvironmentInjector), (evt) => { + events.push(evt); + }), + ) + .subscribe( + (x) => (result = !!x.guardsResult), + (e) => { throw e; - }); + }, + ); expect(result).toBe(true); expect(events.length).toEqual(2); @@ -285,42 +331,58 @@ describe('Router', () => { * great-greatgrandchild */ let result = false; - const childSnapshot = - createActivatedRouteSnapshot({component: 'child', routeConfig: {path: 'child'}}); - const grandchildSnapshot = createActivatedRouteSnapshot( - {component: 'grandchild', routeConfig: {path: 'grandchild'}}); - const greatGrandchildSnapshot = createActivatedRouteSnapshot( - {component: 'greatgrandchild', routeConfig: {path: 'greatgrandchild'}}); - const greatGreatGrandchildSnapshot = createActivatedRouteSnapshot( - {component: 'great-greatgrandchild', routeConfig: {path: 'great-greatgrandchild'}}); + const childSnapshot = createActivatedRouteSnapshot({ + component: 'child', + routeConfig: {path: 'child'}, + }); + const grandchildSnapshot = createActivatedRouteSnapshot({ + component: 'grandchild', + routeConfig: {path: 'grandchild'}, + }); + const greatGrandchildSnapshot = createActivatedRouteSnapshot({ + component: 'greatgrandchild', + routeConfig: {path: 'greatgrandchild'}, + }); + const greatGreatGrandchildSnapshot = createActivatedRouteSnapshot({ + component: 'great-greatgrandchild', + routeConfig: {path: 'great-greatgrandchild'}, + }); const currentState = new (RouterStateSnapshot as any)( - 'url', - new TreeNode( - empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])])); + 'url', + new TreeNode(empty.root, [ + new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]), + ]), + ); const futureState = new (RouterStateSnapshot as any)( - 'url', - new TreeNode( - empty.root, - [new TreeNode( - childSnapshot, [new TreeNode(grandchildSnapshot, [ - new TreeNode( - greatGrandchildSnapshot, [new TreeNode(greatGreatGrandchildSnapshot, [])]) - ])])])); + 'url', + new TreeNode(empty.root, [ + new TreeNode(childSnapshot, [ + new TreeNode(grandchildSnapshot, [ + new TreeNode(greatGrandchildSnapshot, [ + new TreeNode(greatGreatGrandchildSnapshot, []), + ]), + ]), + ]), + ]), + ); // Since we only test the guards, we don't need to provide a full navigation // transition object with all properties set. const testTransition = { - guards: getAllRouteGuards(futureState, currentState, new ChildrenOutletContexts()) + guards: getAllRouteGuards(futureState, currentState, new ChildrenOutletContexts()), } as NavigationTransition; of(testTransition) - .pipe(checkGuardsOperator( - TestBed.inject(EnvironmentInjector), - (evt) => { - events.push(evt); - })) - .subscribe((x) => result = !!x.guardsResult, (e) => { + .pipe( + checkGuardsOperator(TestBed.inject(EnvironmentInjector), (evt) => { + events.push(evt); + }), + ) + .subscribe( + (x) => (result = !!x.guardsResult), + (e) => { throw e; - }); + }, + ); expect(result).toBe(true); expect(events.length).toEqual(4); @@ -344,18 +406,21 @@ describe('Router', () => { const childSnapshot = createActivatedRouteSnapshot({ component: 'child', routeConfig: { - canActivate: [CA_CHILD], - canActivateChild: [CAC_CHILD] - } + canActivateChild: [CAC_CHILD], + }, + }); + const grandchildSnapshot = createActivatedRouteSnapshot({ + component: 'grandchild', + routeConfig: {canActivate: [CA_GRANDCHILD]}, }); - const grandchildSnapshot = createActivatedRouteSnapshot( - {component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}}); const futureState = new (RouterStateSnapshot as any)( - 'url', - new TreeNode( - empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])])); + 'url', + new TreeNode(empty.root, [ + new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]), + ]), + ); checkGuards(futureState, empty, TestBed.inject(EnvironmentInjector), (result) => { expect(result).toBe(true); @@ -374,15 +439,19 @@ describe('Router', () => { const childSnapshot = createActivatedRouteSnapshot({ component: 'child', - routeConfig: {canActivate: [CA_CHILD_FALSE], canActivateChild: [CAC_CHILD]} + routeConfig: {canActivate: [CA_CHILD_FALSE], canActivateChild: [CAC_CHILD]}, + }); + const grandchildSnapshot = createActivatedRouteSnapshot({ + component: 'grandchild', + routeConfig: {canActivate: [CA_GRANDCHILD]}, }); - const grandchildSnapshot = createActivatedRouteSnapshot( - {component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}}); const futureState = new (RouterStateSnapshot as any)( - 'url', - new TreeNode( - empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])])); + 'url', + new TreeNode(empty.root, [ + new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]), + ]), + ); checkGuards(futureState, empty, TestBed.inject(EnvironmentInjector), (result) => { expect(result).toBe(false); @@ -401,15 +470,19 @@ describe('Router', () => { const childSnapshot = createActivatedRouteSnapshot({ component: 'child', - routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD_FALSE]} + routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD_FALSE]}, + }); + const grandchildSnapshot = createActivatedRouteSnapshot({ + component: 'grandchild', + routeConfig: {canActivate: [CA_GRANDCHILD]}, }); - const grandchildSnapshot = createActivatedRouteSnapshot( - {component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}}); const futureState = new (RouterStateSnapshot as any)( - 'url', - new TreeNode( - empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])])); + 'url', + new TreeNode(empty.root, [ + new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]), + ]), + ); checkGuards(futureState, empty, TestBed.inject(EnvironmentInjector), (result) => { expect(result).toBe(false); @@ -426,24 +499,32 @@ describe('Router', () => { * grandchild (CA) */ - const prevSnapshot = createActivatedRouteSnapshot( - {component: 'prev', routeConfig: {canDeactivate: [CDA_CHILD]}}); + const prevSnapshot = createActivatedRouteSnapshot({ + component: 'prev', + routeConfig: {canDeactivate: [CDA_CHILD]}, + }); const childSnapshot = createActivatedRouteSnapshot({ component: 'child', - routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]} + routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]}, }); - const grandchildSnapshot = createActivatedRouteSnapshot( - {component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}}); + const grandchildSnapshot = createActivatedRouteSnapshot({ + component: 'grandchild', + routeConfig: {canActivate: [CA_GRANDCHILD]}, + }); const currentState = new (RouterStateSnapshot as any)( - 'prev', new TreeNode(empty.root, [new TreeNode(prevSnapshot, [])])); + 'prev', + new TreeNode(empty.root, [new TreeNode(prevSnapshot, [])]), + ); const futureState = new (RouterStateSnapshot as any)( - 'url', - new TreeNode( - empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])])); + 'url', + new TreeNode(empty.root, [ + new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]), + ]), + ); checkGuards(futureState, currentState, TestBed.inject(EnvironmentInjector), (result) => { expect(logger.logs).toEqual([CDA_CHILD, CA_CHILD, CAC_CHILD, CA_GRANDCHILD]); @@ -459,21 +540,29 @@ describe('Router', () => { * grandchild (CA) */ - const prevSnapshot = createActivatedRouteSnapshot( - {component: 'prev', routeConfig: {canDeactivate: [CDA_CHILD_FALSE]}}); + const prevSnapshot = createActivatedRouteSnapshot({ + component: 'prev', + routeConfig: {canDeactivate: [CDA_CHILD_FALSE]}, + }); const childSnapshot = createActivatedRouteSnapshot({ component: 'child', - routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]} + routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]}, + }); + const grandchildSnapshot = createActivatedRouteSnapshot({ + component: 'grandchild', + routeConfig: {canActivate: [CA_GRANDCHILD]}, }); - const grandchildSnapshot = createActivatedRouteSnapshot( - {component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}}); const currentState = new (RouterStateSnapshot as any)( - 'prev', new TreeNode(empty.root, [new TreeNode(prevSnapshot, [])])); + 'prev', + new TreeNode(empty.root, [new TreeNode(prevSnapshot, [])]), + ); const futureState = new (RouterStateSnapshot as any)( - 'url', - new TreeNode( - empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])])); + 'url', + new TreeNode(empty.root, [ + new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]), + ]), + ); checkGuards(futureState, currentState, TestBed.inject(EnvironmentInjector), (result) => { expect(result).toBe(false); @@ -489,31 +578,45 @@ describe('Router', () => { * prevGrandchild(CDA) grandchild (CA) */ - const prevChildSnapshot = createActivatedRouteSnapshot( - {component: 'prev_child', routeConfig: {canDeactivate: [CDA_CHILD]}}); - const prevGrandchildSnapshot = createActivatedRouteSnapshot( - {component: 'prev_grandchild', routeConfig: {canDeactivate: [CDA_GRANDCHILD]}}); + const prevChildSnapshot = createActivatedRouteSnapshot({ + component: 'prev_child', + routeConfig: {canDeactivate: [CDA_CHILD]}, + }); + const prevGrandchildSnapshot = createActivatedRouteSnapshot({ + component: 'prev_grandchild', + routeConfig: {canDeactivate: [CDA_GRANDCHILD]}, + }); const childSnapshot = createActivatedRouteSnapshot({ component: 'child', - routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]} + routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]}, + }); + const grandchildSnapshot = createActivatedRouteSnapshot({ + component: 'grandchild', + routeConfig: {canActivate: [CA_GRANDCHILD]}, }); - const grandchildSnapshot = createActivatedRouteSnapshot( - {component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}}); const currentState = new (RouterStateSnapshot as any)( - 'prev', new TreeNode(empty.root, [ - new TreeNode(prevChildSnapshot, [new TreeNode(prevGrandchildSnapshot, [])]) - ])); + 'prev', + new TreeNode(empty.root, [ + new TreeNode(prevChildSnapshot, [new TreeNode(prevGrandchildSnapshot, [])]), + ]), + ); const futureState = new (RouterStateSnapshot as any)( - 'url', - new TreeNode( - empty.root, [new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])])])); + 'url', + new TreeNode(empty.root, [ + new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]), + ]), + ); checkGuards(futureState, currentState, TestBed.inject(EnvironmentInjector), (result) => { expect(result).toBe(true); expect(logger.logs).toEqual([ - CDA_GRANDCHILD, CDA_CHILD, CA_CHILD, CAC_CHILD, CA_GRANDCHILD + CDA_GRANDCHILD, + CDA_CHILD, + CA_CHILD, + CAC_CHILD, + CA_GRANDCHILD, ]); }); @@ -535,13 +638,14 @@ describe('Router', () => { const childSnapshot = createActivatedRouteSnapshot({ component: 'child', routeConfig: { - - canActivate: [CA_CHILD_REDIRECT] - } + canActivate: [CA_CHILD_REDIRECT], + }, }); const futureState = new (RouterStateSnapshot as any)( - 'url', new TreeNode(empty.root, [new TreeNode(childSnapshot, [])])); + 'url', + new TreeNode(empty.root, [new TreeNode(childSnapshot, [])]), + ); checkGuards(futureState, empty, TestBed.inject(EnvironmentInjector), (result) => { expect(serializer.serialize(result as UrlTree)).toBe('/' + CA_CHILD_REDIRECT); @@ -558,15 +662,21 @@ describe('Router', () => { * grandchild (CA) */ - const childSnapshot = createActivatedRouteSnapshot( - {component: 'child', routeConfig: {canActivateChild: [CAC_CHILD_REDIRECT]}}); - const grandchildSnapshot = createActivatedRouteSnapshot( - {component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}}); + const childSnapshot = createActivatedRouteSnapshot({ + component: 'child', + routeConfig: {canActivateChild: [CAC_CHILD_REDIRECT]}, + }); + const grandchildSnapshot = createActivatedRouteSnapshot({ + component: 'grandchild', + routeConfig: {canActivate: [CA_GRANDCHILD]}, + }); const futureState = new (RouterStateSnapshot as any)( - 'url', new TreeNode(empty.root, [ - new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]) - ])); + 'url', + new TreeNode(empty.root, [ + new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]), + ]), + ); checkGuards(futureState, empty, TestBed.inject(EnvironmentInjector), (result) => { expect(serializer.serialize(result as UrlTree)).toBe('/' + CAC_CHILD_REDIRECT); @@ -583,15 +693,21 @@ describe('Router', () => { * grandchild (CA: redirect) */ - const childSnapshot = createActivatedRouteSnapshot( - {component: 'child', routeConfig: {canActivateChild: [CAC_CHILD]}}); - const grandchildSnapshot = createActivatedRouteSnapshot( - {component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD_REDIRECT]}}); + const childSnapshot = createActivatedRouteSnapshot({ + component: 'child', + routeConfig: {canActivateChild: [CAC_CHILD]}, + }); + const grandchildSnapshot = createActivatedRouteSnapshot({ + component: 'grandchild', + routeConfig: {canActivate: [CA_GRANDCHILD_REDIRECT]}, + }); const futureState = new (RouterStateSnapshot as any)( - 'url', new TreeNode(empty.root, [ - new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]) - ])); + 'url', + new TreeNode(empty.root, [ + new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]), + ]), + ); checkGuards(futureState, empty, TestBed.inject(EnvironmentInjector), (result) => { expect(serializer.serialize(result as UrlTree)).toBe('/' + CA_GRANDCHILD_REDIRECT); @@ -608,21 +724,29 @@ describe('Router', () => { * grandchild (CA) */ - const prevSnapshot = createActivatedRouteSnapshot( - {component: 'prev', routeConfig: {canDeactivate: [CDA_CHILD_REDIRECT]}}); + const prevSnapshot = createActivatedRouteSnapshot({ + component: 'prev', + routeConfig: {canDeactivate: [CDA_CHILD_REDIRECT]}, + }); const childSnapshot = createActivatedRouteSnapshot({ component: 'child', - routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]} + routeConfig: {canActivate: [CA_CHILD], canActivateChild: [CAC_CHILD]}, + }); + const grandchildSnapshot = createActivatedRouteSnapshot({ + component: 'grandchild', + routeConfig: {canActivate: [CA_GRANDCHILD]}, }); - const grandchildSnapshot = createActivatedRouteSnapshot( - {component: 'grandchild', routeConfig: {canActivate: [CA_GRANDCHILD]}}); const currentState = new (RouterStateSnapshot as any)( - 'prev', new TreeNode(empty.root, [new TreeNode(prevSnapshot, [])])); + 'prev', + new TreeNode(empty.root, [new TreeNode(prevSnapshot, [])]), + ); const futureState = new (RouterStateSnapshot as any)( - 'url', new TreeNode(empty.root, [ - new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]) - ])); + 'url', + new TreeNode(empty.root, [ + new TreeNode(childSnapshot, [new TreeNode(grandchildSnapshot, [])]), + ]), + ); checkGuards(futureState, currentState, TestBed.inject(EnvironmentInjector), (result) => { expect(serializer.serialize(result as UrlTree)).toBe('/' + CDA_CHILD_REDIRECT); @@ -642,7 +766,9 @@ describe('Router', () => { const r = {data: () => 'resolver_value'}; const n = createActivatedRouteSnapshot({component: 'a', resolve: r}); const s = new (RouterStateSnapshot as any)( - 'url', new TreeNode(empty.root, [new TreeNode(n, [])])); + 'url', + new TreeNode(empty.root, [new TreeNode(n, [])]), + ); checkResolveData(s, empty, TestBed.inject(EnvironmentInjector), () => { expect(s.root.firstChild!.data).toEqual({data: 'resolver_value'}); @@ -664,7 +790,9 @@ describe('Router', () => { const child = createActivatedRouteSnapshot({component: 'b', resolve: childResolve}); const s = new (RouterStateSnapshot as any)( - 'url', new TreeNode(empty.root, [new TreeNode(parent, [new TreeNode(child, [])])])); + 'url', + new TreeNode(empty.root, [new TreeNode(parent, [new TreeNode(child, [])])]), + ); checkResolveData(s, empty, TestBed.inject(EnvironmentInjector), () => { expect(s.root.firstChild!.firstChild!.data).toEqual({data: 'resolver_value'}); @@ -684,13 +812,17 @@ describe('Router', () => { const n1 = createActivatedRouteSnapshot({component: 'a', resolve: r1}); const s1 = new (RouterStateSnapshot as any)( - 'url', new TreeNode(empty.root, [new TreeNode(n1, [])])); + 'url', + new TreeNode(empty.root, [new TreeNode(n1, [])]), + ); checkResolveData(s1, empty, TestBed.inject(EnvironmentInjector), () => {}); const n21 = createActivatedRouteSnapshot({component: 'a', resolve: r1}); const n22 = createActivatedRouteSnapshot({component: 'b', resolve: r2}); const s2 = new (RouterStateSnapshot as any)( - 'url', new TreeNode(empty.root, [new TreeNode(n21, [new TreeNode(n22, [])])])); + 'url', + new TreeNode(empty.root, [new TreeNode(n21, [new TreeNode(n22, [])])]), + ); checkResolveData(s2, s1, TestBed.inject(EnvironmentInjector), () => { expect(s2.root.firstChild!.data).toEqual({data: 'resolver1_value'}); expect(s2.root.firstChild!.firstChild!.data).toEqual({data: 'resolver2_value'}); @@ -701,33 +833,41 @@ describe('Router', () => { }); function checkResolveData( - future: RouterStateSnapshot, curr: RouterStateSnapshot, injector: EnvironmentInjector, - check: any): void { + future: RouterStateSnapshot, + curr: RouterStateSnapshot, + injector: EnvironmentInjector, + check: any, +): void { // Since we only test the guards and their resolve data function, we don't need to provide // a full navigation transition object with all properties set. - of({guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts())} as - NavigationTransition) - .pipe(resolveDataOperator('emptyOnly', injector)) - .subscribe(check, (e) => { - throw e; - }); + of({ + guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts()), + } as NavigationTransition) + .pipe(resolveDataOperator('emptyOnly', injector)) + .subscribe(check, (e) => { + throw e; + }); } function checkGuards( - future: RouterStateSnapshot, curr: RouterStateSnapshot, injector: EnvironmentInjector, - check: (result: boolean|UrlTree) => void): void { + future: RouterStateSnapshot, + curr: RouterStateSnapshot, + injector: EnvironmentInjector, + check: (result: boolean | UrlTree) => void, +): void { // Since we only test the guards, we don't need to provide a full navigation // transition object with all properties set. - of({guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts())} as - NavigationTransition) - .pipe(checkGuardsOperator(injector)) - .subscribe({ - next(t) { - if (t.guardsResult === null) throw new Error('Guard result expected'); - return check(t.guardsResult); - }, - error(e) { - throw e; - } - }); + of({ + guards: getAllRouteGuards(future, curr, new ChildrenOutletContexts()), + } as NavigationTransition) + .pipe(checkGuardsOperator(injector)) + .subscribe({ + next(t) { + if (t.guardsResult === null) throw new Error('Guard result expected'); + return check(t.guardsResult); + }, + error(e) { + throw e; + }, + }); } diff --git a/packages/router/test/router_link_spec.ts b/packages/router/test/router_link_spec.ts index e792e5b297b..82659f75d75 100644 --- a/packages/router/test/router_link_spec.ts +++ b/packages/router/test/router_link_spec.ts @@ -15,10 +15,12 @@ describe('RouterLink', () => { it('does not modify tabindex if already set on non-anchor element', () => { @Component({template: `
`}) class LinkComponent { - link: string|null|undefined = '/'; + link: string | null | undefined = '/'; } - TestBed.configureTestingModule( - {imports: [RouterModule.forRoot([])], declarations: [LinkComponent]}); + TestBed.configureTestingModule({ + imports: [RouterModule.forRoot([])], + declarations: [LinkComponent], + }); const fixture = TestBed.createComponent(LinkComponent); fixture.detectChanges(); const link = fixture.debugElement.query(By.css('div')).nativeElement; @@ -37,10 +39,10 @@ describe('RouterLink', () => { [preserveFragment]="preserveFragment" [skipLocationChange]="skipLocationChange" [replaceUrl]="replaceUrl"> - ` + `, }) class LinkComponent { - link: string|null|undefined = '/'; + link: string | null | undefined = '/'; preserveFragment: unknown; skipLocationChange: unknown; replaceUrl: unknown; @@ -117,10 +119,10 @@ describe('RouterLink', () => { [preserveFragment]="preserveFragment" [skipLocationChange]="skipLocationChange" [replaceUrl]="replaceUrl"> - ` + `, }) class LinkComponent { - link: string|null|undefined = '/'; + link: string | null | undefined = '/'; preserveFragment: unknown; skipLocationChange: unknown; replaceUrl: unknown; @@ -179,8 +181,7 @@ describe('RouterLink', () => { it('should handle routerLink in svg templates', () => { @Component({template: ``}) - class LinkComponent { - } + class LinkComponent {} TestBed.configureTestingModule({ imports: [RouterModule.forRoot([])], diff --git a/packages/router/test/router_preloader.spec.ts b/packages/router/test/router_preloader.spec.ts index 1d41bdbf43a..6c077d52bd7 100644 --- a/packages/router/test/router_preloader.spec.ts +++ b/packages/router/test/router_preloader.spec.ts @@ -7,23 +7,42 @@ */ import {provideLocationMocks} from '@angular/common/testing'; -import {Compiler, Component, Injectable, InjectionToken, Injector, NgModule, NgModuleFactory, NgModuleRef, Type} from '@angular/core'; +import { + Compiler, + Component, + Injectable, + InjectionToken, + Injector, + NgModule, + NgModuleFactory, + NgModuleRef, + Type, +} from '@angular/core'; import {R3Injector} from '@angular/core/src/di/r3_injector'; import {NgModuleRef as R3NgModuleRef} from '@angular/core/src/render3'; import {fakeAsync, inject, TestBed, tick} from '@angular/core/testing'; -import {PreloadAllModules, PreloadingStrategy, RouterPreloader, ROUTES, withPreloading} from '@angular/router'; +import { + PreloadAllModules, + PreloadingStrategy, + RouterPreloader, + ROUTES, + withPreloading, +} from '@angular/router'; import {BehaviorSubject, Observable, of, throwError} from 'rxjs'; import {catchError, delay, filter, switchMap, take} from 'rxjs/operators'; import {Route, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouterModule} from '../index'; import {provideRouter} from '../src/provide_router'; -import {getLoadedComponent, getLoadedInjector, getLoadedRoutes, getProvidersInjector} from '../src/utils/config'; - +import { + getLoadedComponent, + getLoadedInjector, + getLoadedRoutes, + getProvidersInjector, +} from '../src/utils/config'; describe('RouterPreloader', () => { @Component({template: ''}) - class LazyLoadedCmp { - } + class LazyLoadedCmp {} describe('should properly handle', () => { beforeEach(() => { @@ -31,9 +50,10 @@ describe('RouterPreloader', () => { providers: [ provideLocationMocks(), provideRouter( - [{path: 'lazy', loadChildren: jasmine.createSpy('expected'), canLoad: ['someGuard']}], - withPreloading(PreloadAllModules)), - ] + [{path: 'lazy', loadChildren: jasmine.createSpy('expected'), canLoad: ['someGuard']}], + withPreloading(PreloadAllModules), + ), + ], }); }); @@ -52,45 +72,44 @@ describe('RouterPreloader', () => { { provide: ROUTES, multi: true, - useValue: [{path: 'LoadedModule1', component: LazyLoadedCmp}] + useValue: [{path: 'LoadedModule1', component: LazyLoadedCmp}], }, - ] + ], }) - class LoadedModule { - } + class LoadedModule {} beforeEach(() => { TestBed.configureTestingModule({ providers: [ provideLocationMocks(), provideRouter( - [{path: 'lazy', loadChildren: () => LoadedModule, canLoad: ['someGuard']}], - withPreloading(PreloadAllModules)), + [{path: 'lazy', loadChildren: () => LoadedModule, canLoad: ['someGuard']}], + withPreloading(PreloadAllModules), + ), ], }); }); + it('should not load children', fakeAsync( + inject([RouterPreloader, Router], (preloader: RouterPreloader, router: Router) => { + preloader.preload().subscribe(() => {}); - it('should not load children', - fakeAsync(inject([RouterPreloader, Router], (preloader: RouterPreloader, router: Router) => { - preloader.preload().subscribe(() => {}); + tick(); - tick(); + const c = router.config; + expect((c[0] as any)._loadedRoutes).not.toBeDefined(); + }), + )); - const c = router.config; - expect((c[0] as any)._loadedRoutes).not.toBeDefined(); - }))); + it('should not call the preloading method because children will not be loaded anyways', fakeAsync(() => { + const preloader = TestBed.inject(RouterPreloader); + const preloadingStrategy = TestBed.inject(PreloadingStrategy); + spyOn(preloadingStrategy, 'preload').and.callThrough(); + preloader.preload().subscribe(() => {}); - it('should not call the preloading method because children will not be loaded anyways', - fakeAsync(() => { - const preloader = TestBed.inject(RouterPreloader); - const preloadingStrategy = TestBed.inject(PreloadingStrategy); - spyOn(preloadingStrategy, 'preload').and.callThrough(); - preloader.preload().subscribe(() => {}); - - tick(); - expect(preloadingStrategy.preload).not.toHaveBeenCalled(); - })); + tick(); + expect(preloadingStrategy.preload).not.toHaveBeenCalled(); + })); }); describe('should preload configurations', () => { @@ -105,96 +124,101 @@ describe('RouterPreloader', () => { }); }); - it('should work', - fakeAsync(inject( - [RouterPreloader, Router, NgModuleRef], - (preloader: RouterPreloader, router: Router, testModule: R3NgModuleRef) => { - const events: Array = []; - @NgModule({ - declarations: [LazyLoadedCmp], - imports: [RouterModule.forChild([{path: 'LoadedModule2', component: LazyLoadedCmp}])] - }) - class LoadedModule2 { - } + it('should work', fakeAsync( + inject( + [RouterPreloader, Router, NgModuleRef], + (preloader: RouterPreloader, router: Router, testModule: R3NgModuleRef) => { + const events: Array = []; + @NgModule({ + declarations: [LazyLoadedCmp], + imports: [RouterModule.forChild([{path: 'LoadedModule2', component: LazyLoadedCmp}])], + }) + class LoadedModule2 {} - @NgModule({ - imports: [RouterModule.forChild( - [{path: 'LoadedModule1', loadChildren: () => LoadedModule2}])] - }) - class LoadedModule1 { - } + @NgModule({ + imports: [ + RouterModule.forChild([{path: 'LoadedModule1', loadChildren: () => LoadedModule2}]), + ], + }) + class LoadedModule1 {} - router.events.subscribe(e => { - if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { - events.push(e); - } - }); + router.events.subscribe((e) => { + if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { + events.push(e); + } + }); - lazySpy.and.returnValue(LoadedModule1); - preloader.preload().subscribe(() => {}); + lazySpy.and.returnValue(LoadedModule1); + preloader.preload().subscribe(() => {}); - tick(); + tick(); - const c = router.config; - const injector: any = getLoadedInjector(c[0]); - const loadedRoutes: Route[] = getLoadedRoutes(c[0])!; - expect(loadedRoutes[0].path).toEqual('LoadedModule1'); - expect(injector.parent).toBe(testModule._r3Injector); + const c = router.config; + const injector: any = getLoadedInjector(c[0]); + const loadedRoutes: Route[] = getLoadedRoutes(c[0])!; + expect(loadedRoutes[0].path).toEqual('LoadedModule1'); + expect(injector.parent).toBe(testModule._r3Injector); - const injector2: any = getLoadedInjector(loadedRoutes[0]); - const loadedRoutes2: Route[] = getLoadedRoutes(loadedRoutes[0])!; - expect(loadedRoutes2[0].path).toEqual('LoadedModule2'); - expect(injector2.parent).toBe(injector); + const injector2: any = getLoadedInjector(loadedRoutes[0]); + const loadedRoutes2: Route[] = getLoadedRoutes(loadedRoutes[0])!; + expect(loadedRoutes2[0].path).toEqual('LoadedModule2'); + expect(injector2.parent).toBe(injector); - expect(events.map(e => e.toString())).toEqual([ - 'RouteConfigLoadStart(path: lazy)', - 'RouteConfigLoadEnd(path: lazy)', - 'RouteConfigLoadStart(path: LoadedModule1)', - 'RouteConfigLoadEnd(path: LoadedModule1)', - ]); - }))); + expect(events.map((e) => e.toString())).toEqual([ + 'RouteConfigLoadStart(path: lazy)', + 'RouteConfigLoadEnd(path: lazy)', + 'RouteConfigLoadStart(path: LoadedModule1)', + 'RouteConfigLoadEnd(path: LoadedModule1)', + ]); + }, + ), + )); }); it('should handle providers on a route', fakeAsync(() => { - const TOKEN = new InjectionToken('test token'); - const CHILD_TOKEN = new InjectionToken('test token for child'); + const TOKEN = new InjectionToken('test token'); + const CHILD_TOKEN = new InjectionToken('test token for child'); - @NgModule({ - imports: [RouterModule.forChild([{path: 'child', redirectTo: ''}])], - providers: [{provide: CHILD_TOKEN, useValue: 'child'}] - }) - class Child { - } + @NgModule({ + imports: [RouterModule.forChild([{path: 'child', redirectTo: ''}])], + providers: [{provide: CHILD_TOKEN, useValue: 'child'}], + }) + class Child {} - TestBed.configureTestingModule({ - providers: [ - provideLocationMocks(), - provideRouter( - [{ - path: 'parent', - providers: [{provide: TOKEN, useValue: 'parent'}], - loadChildren: () => Child, - }], - withPreloading(PreloadAllModules)), - ], - }); + TestBed.configureTestingModule({ + providers: [ + provideLocationMocks(), + provideRouter( + [ + { + path: 'parent', + providers: [{provide: TOKEN, useValue: 'parent'}], + loadChildren: () => Child, + }, + ], + withPreloading(PreloadAllModules), + ), + ], + }); - TestBed.inject(RouterPreloader).preload().subscribe(() => {}); + TestBed.inject(RouterPreloader) + .preload() + .subscribe(() => {}); - tick(); + tick(); - const parentConfig = TestBed.inject(Router).config[0]; - // preloading needs to create the injector - const providersInjector = getProvidersInjector(parentConfig); - expect(providersInjector).toBeDefined(); - // Throws error because there is no provider for CHILD_TOKEN here - expect(() => providersInjector?.get(CHILD_TOKEN)).toThrow(); + const parentConfig = TestBed.inject(Router).config[0]; + // preloading needs to create the injector + const providersInjector = getProvidersInjector(parentConfig); + expect(providersInjector).toBeDefined(); + // Throws error because there is no provider for CHILD_TOKEN here + expect(() => providersInjector?.get(CHILD_TOKEN)).toThrow(); - const loadedInjector = getLoadedInjector(parentConfig)!; - // // The loaded injector should be a child of the one created from providers - expect(loadedInjector.get(TOKEN)).toEqual('parent'); - expect(loadedInjector.get(CHILD_TOKEN)).toEqual('child'); - })); + const loadedInjector = getLoadedInjector(parentConfig)!; + // // The loaded injector should be a child of the one created from providers + expect(loadedInjector.get(TOKEN)).toEqual('parent'); + expect(loadedInjector.get(CHILD_TOKEN)).toEqual('child'); + })); describe('should support modules that have already been loaded', () => { let lazySpy: jasmine.Spy; @@ -208,56 +232,61 @@ describe('RouterPreloader', () => { }); }); - it('should work', - fakeAsync(inject( - [RouterPreloader, Router, NgModuleRef, Compiler], - (preloader: RouterPreloader, router: Router, testModule: R3NgModuleRef, - compiler: Compiler) => { - @NgModule() - class LoadedModule2 { - } + it('should work', fakeAsync( + inject( + [RouterPreloader, Router, NgModuleRef, Compiler], + ( + preloader: RouterPreloader, + router: Router, + testModule: R3NgModuleRef, + compiler: Compiler, + ) => { + @NgModule() + class LoadedModule2 {} - const module2 = compiler.compileModuleSync(LoadedModule2).create(null); + const module2 = compiler.compileModuleSync(LoadedModule2).create(null); - @NgModule({ - imports: [RouterModule.forChild([ - { - path: 'LoadedModule2', - loadChildren: jasmine.createSpy('no'), - _loadedRoutes: [{path: 'LoadedModule3', loadChildren: () => LoadedModule3}], - _loadedInjector: module2.injector, - }, - ])] - }) - class LoadedModule1 { - } + @NgModule({ + imports: [ + RouterModule.forChild([ + { + path: 'LoadedModule2', + loadChildren: jasmine.createSpy('no'), + _loadedRoutes: [{path: 'LoadedModule3', loadChildren: () => LoadedModule3}], + _loadedInjector: module2.injector, + }, + ]), + ], + }) + class LoadedModule1 {} - @NgModule({imports: [RouterModule.forChild([])]}) - class LoadedModule3 { - } + @NgModule({imports: [RouterModule.forChild([])]}) + class LoadedModule3 {} - lazySpy.and.returnValue(LoadedModule1); - preloader.preload().subscribe(() => {}); + lazySpy.and.returnValue(LoadedModule1); + preloader.preload().subscribe(() => {}); - tick(); + tick(); - const c = router.config; + const c = router.config; - const injector = getLoadedInjector(c[0]) as R3Injector; + const injector = getLoadedInjector(c[0]) as R3Injector; - const loadedRoutes = getLoadedRoutes(c[0])!; - expect(injector.parent).toBe((testModule)._r3Injector); + const loadedRoutes = getLoadedRoutes(c[0])!; + expect(injector.parent).toBe(testModule._r3Injector); - const loadedRoutes2: Route[] = getLoadedRoutes(loadedRoutes[0])!; - const injector3 = getLoadedInjector(loadedRoutes2[0]) as R3Injector; - expect(injector3.parent).toBe(module2.injector); - }))); + const loadedRoutes2: Route[] = getLoadedRoutes(loadedRoutes[0])!; + const injector3 = getLoadedInjector(loadedRoutes2[0]) as R3Injector; + expect(injector3.parent).toBe(module2.injector); + }, + ), + )); }); describe('should support preloading strategies', () => { let delayLoadUnPaused: BehaviorSubject; let delayLoadObserver$: Observable; - let events: Array; + let events: Array; const subLoadChildrenSpy = jasmine.createSpy('submodule'); const lazyLoadChildrenSpy = jasmine.createSpy('lazymodule'); @@ -265,14 +294,15 @@ describe('RouterPreloader', () => { const mockPreloaderFactory = (): PreloadingStrategy => { class DelayedPreLoad implements PreloadingStrategy { preload(route: Route, fn: () => Observable): Observable { - const routeName = - route.loadChildren ? (route.loadChildren as jasmine.Spy).and.identity : 'noChildren'; + const routeName = route.loadChildren + ? (route.loadChildren as jasmine.Spy).and.identity + : 'noChildren'; return delayLoadObserver$.pipe( - filter(unpauseList => unpauseList.indexOf(routeName) !== -1), - take(1), - switchMap(() => { - return fn().pipe(catchError(() => of(null))); - }), + filter((unpauseList) => unpauseList.indexOf(routeName) !== -1), + take(1), + switchMap(() => { + return fn().pipe(catchError(() => of(null))); + }), ); } } @@ -282,26 +312,26 @@ describe('RouterPreloader', () => { @NgModule({ declarations: [LazyLoadedCmp], }) - class SharedModule { - } + class SharedModule {} @NgModule({ imports: [ - SharedModule, RouterModule.forChild([ + SharedModule, + RouterModule.forChild([ {path: 'LoadedModule1', component: LazyLoadedCmp}, - {path: 'sub', loadChildren: subLoadChildrenSpy} - ]) - ] + {path: 'sub', loadChildren: subLoadChildrenSpy}, + ]), + ], }) - class LoadedModule1 { - } + class LoadedModule1 {} @NgModule({ - imports: - [SharedModule, RouterModule.forChild([{path: 'LoadedModule2', component: LazyLoadedCmp}])] + imports: [ + SharedModule, + RouterModule.forChild([{path: 'LoadedModule2', component: LazyLoadedCmp}]), + ], }) - class LoadedModule2 { - } + class LoadedModule2 {} beforeEach(() => { delayLoadUnPaused = new BehaviorSubject([]); @@ -313,261 +343,271 @@ describe('RouterPreloader', () => { provideLocationMocks(), provideRouter([{path: 'lazy', loadChildren: lazyLoadChildrenSpy}]), {provide: PreloadingStrategy, useFactory: mockPreloaderFactory}, - ] + ], }); events = []; }); it('without reloading loaded modules', fakeAsync(() => { - const preloader = TestBed.inject(RouterPreloader); - const router = TestBed.inject(Router); - router.events.subscribe(e => { - if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { - events.push(e); - } - }); - lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1)); + const preloader = TestBed.inject(RouterPreloader); + const router = TestBed.inject(Router); + router.events.subscribe((e) => { + if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { + events.push(e); + } + }); + lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1)); - // App start activation of preloader - preloader.preload().subscribe((x) => {}); - tick(); - expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(0); + // App start activation of preloader + preloader.preload().subscribe((x) => {}); + tick(); + expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(0); - // Initial navigation cause route load - router.navigateByUrl('/lazy/LoadedModule1'); - tick(); - expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); + // Initial navigation cause route load + router.navigateByUrl('/lazy/LoadedModule1'); + tick(); + expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); - // Secondary load or navigation should use same loaded object ( - // ie this is a noop as the module should already be loaded) - delayLoadUnPaused.next(['lazymodule']); - tick(); - expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); - expect(subLoadChildrenSpy).toHaveBeenCalledTimes(0); - expect(events.map(e => e.toString())).toEqual([ - 'RouteConfigLoadStart(path: lazy)', 'RouteConfigLoadEnd(path: lazy)' - ]); - })); + // Secondary load or navigation should use same loaded object ( + // ie this is a noop as the module should already be loaded) + delayLoadUnPaused.next(['lazymodule']); + tick(); + expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); + expect(subLoadChildrenSpy).toHaveBeenCalledTimes(0); + expect(events.map((e) => e.toString())).toEqual([ + 'RouteConfigLoadStart(path: lazy)', + 'RouteConfigLoadEnd(path: lazy)', + ]); + })); - it('and cope with the loader throwing exceptions during module load but allow retry', - fakeAsync(() => { - const preloader = TestBed.inject(RouterPreloader); - const router = TestBed.inject(Router); - router.events.subscribe(e => { - if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { - events.push(e); - } - }); + it('and cope with the loader throwing exceptions during module load but allow retry', fakeAsync(() => { + const preloader = TestBed.inject(RouterPreloader); + const router = TestBed.inject(Router); + router.events.subscribe((e) => { + if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { + events.push(e); + } + }); - lazyLoadChildrenSpy.and.returnValue( - throwError('Error: Fake module load error (expectedreload)')); - preloader.preload().subscribe((x) => {}); - tick(); - expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(0); + lazyLoadChildrenSpy.and.returnValue( + throwError('Error: Fake module load error (expectedreload)'), + ); + preloader.preload().subscribe((x) => {}); + tick(); + expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(0); - delayLoadUnPaused.next(['lazymodule']); - tick(); - expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); + delayLoadUnPaused.next(['lazymodule']); + tick(); + expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); - lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1)); - router.navigateByUrl('/lazy/LoadedModule1').catch(() => { - fail('navigation should not throw'); - }); - tick(); - expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(2); - expect(subLoadChildrenSpy).toHaveBeenCalledTimes(0); + lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1)); + router.navigateByUrl('/lazy/LoadedModule1').catch(() => { + fail('navigation should not throw'); + }); + tick(); + expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(2); + expect(subLoadChildrenSpy).toHaveBeenCalledTimes(0); - expect(events.map(e => e.toString())).toEqual([ - 'RouteConfigLoadStart(path: lazy)', 'RouteConfigLoadStart(path: lazy)', - 'RouteConfigLoadEnd(path: lazy)' - ]); - })); + expect(events.map((e) => e.toString())).toEqual([ + 'RouteConfigLoadStart(path: lazy)', + 'RouteConfigLoadStart(path: lazy)', + 'RouteConfigLoadEnd(path: lazy)', + ]); + })); it('and cope with the loader throwing exceptions but allow retry', fakeAsync(() => { - const preloader = TestBed.inject(RouterPreloader); - const router = TestBed.inject(Router); - router.events.subscribe(e => { - if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { - events.push(e); - } - }); + const preloader = TestBed.inject(RouterPreloader); + const router = TestBed.inject(Router); + router.events.subscribe((e) => { + if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { + events.push(e); + } + }); - lazyLoadChildrenSpy.and.returnValue( - throwError('Error: Fake module load error (expectedreload)')); - preloader.preload().subscribe((x) => {}); - tick(); - expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(0); + lazyLoadChildrenSpy.and.returnValue( + throwError('Error: Fake module load error (expectedreload)'), + ); + preloader.preload().subscribe((x) => {}); + tick(); + expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(0); - router.navigateByUrl('/lazy/LoadedModule1').catch((reason) => { - expect(reason).toEqual('Error: Fake module load error (expectedreload)'); - }); - tick(); + router.navigateByUrl('/lazy/LoadedModule1').catch((reason) => { + expect(reason).toEqual('Error: Fake module load error (expectedreload)'); + }); + tick(); - expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); - lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1)); - router.navigateByUrl('/lazy/LoadedModule1').catch(() => { - fail('navigation should not throw'); - }); - tick(); + expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); + lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1)); + router.navigateByUrl('/lazy/LoadedModule1').catch(() => { + fail('navigation should not throw'); + }); + tick(); - expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(2); - expect(subLoadChildrenSpy).toHaveBeenCalledTimes(0); - expect(events.map(e => e.toString())).toEqual([ - 'RouteConfigLoadStart(path: lazy)', 'RouteConfigLoadStart(path: lazy)', - 'RouteConfigLoadEnd(path: lazy)' - ]); - })); + expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(2); + expect(subLoadChildrenSpy).toHaveBeenCalledTimes(0); + expect(events.map((e) => e.toString())).toEqual([ + 'RouteConfigLoadStart(path: lazy)', + 'RouteConfigLoadStart(path: lazy)', + 'RouteConfigLoadEnd(path: lazy)', + ]); + })); it('without autoloading loading submodules', fakeAsync(() => { - const preloader = TestBed.inject(RouterPreloader); - const router = TestBed.inject(Router); - router.events.subscribe(e => { - if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { - events.push(e); - } - }); + const preloader = TestBed.inject(RouterPreloader); + const router = TestBed.inject(Router); + router.events.subscribe((e) => { + if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { + events.push(e); + } + }); - lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1)); - subLoadChildrenSpy.and.returnValue(of(LoadedModule2)); + lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1)); + subLoadChildrenSpy.and.returnValue(of(LoadedModule2)); - preloader.preload().subscribe((x) => {}); - tick(); - router.navigateByUrl('/lazy/LoadedModule1'); - tick(); - expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); - expect(subLoadChildrenSpy).toHaveBeenCalledTimes(0); - expect(events.map(e => e.toString())).toEqual([ - 'RouteConfigLoadStart(path: lazy)', 'RouteConfigLoadEnd(path: lazy)' - ]); + preloader.preload().subscribe((x) => {}); + tick(); + router.navigateByUrl('/lazy/LoadedModule1'); + tick(); + expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); + expect(subLoadChildrenSpy).toHaveBeenCalledTimes(0); + expect(events.map((e) => e.toString())).toEqual([ + 'RouteConfigLoadStart(path: lazy)', + 'RouteConfigLoadEnd(path: lazy)', + ]); - // Release submodule to check it does in fact load - delayLoadUnPaused.next(['lazymodule', 'submodule']); - tick(); - expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); - expect(subLoadChildrenSpy).toHaveBeenCalledTimes(1); - expect(events.map(e => e.toString())).toEqual([ - 'RouteConfigLoadStart(path: lazy)', 'RouteConfigLoadEnd(path: lazy)', - 'RouteConfigLoadStart(path: sub)', 'RouteConfigLoadEnd(path: sub)' - ]); - })); + // Release submodule to check it does in fact load + delayLoadUnPaused.next(['lazymodule', 'submodule']); + tick(); + expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); + expect(subLoadChildrenSpy).toHaveBeenCalledTimes(1); + expect(events.map((e) => e.toString())).toEqual([ + 'RouteConfigLoadStart(path: lazy)', + 'RouteConfigLoadEnd(path: lazy)', + 'RouteConfigLoadStart(path: sub)', + 'RouteConfigLoadEnd(path: sub)', + ]); + })); it('and close the preload obsservable ', fakeAsync(() => { - const preloader = TestBed.inject(RouterPreloader); - const router = TestBed.inject(Router); - router.events.subscribe(e => { - if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { - events.push(e); - } - }); + const preloader = TestBed.inject(RouterPreloader); + const router = TestBed.inject(Router); + router.events.subscribe((e) => { + if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { + events.push(e); + } + }); - lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1)); - subLoadChildrenSpy.and.returnValue(of(LoadedModule2)); - const preloadSubscription = preloader.preload().subscribe((x) => {}); + lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1)); + subLoadChildrenSpy.and.returnValue(of(LoadedModule2)); + const preloadSubscription = preloader.preload().subscribe((x) => {}); - router.navigateByUrl('/lazy/LoadedModule1'); - tick(); - delayLoadUnPaused.next(['lazymodule', 'submodule']); - tick(); + router.navigateByUrl('/lazy/LoadedModule1'); + tick(); + delayLoadUnPaused.next(['lazymodule', 'submodule']); + tick(); - expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); - expect(subLoadChildrenSpy).toHaveBeenCalledTimes(1); - expect(preloadSubscription.closed).toBeTruthy(); - })); + expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); + expect(subLoadChildrenSpy).toHaveBeenCalledTimes(1); + expect(preloadSubscription.closed).toBeTruthy(); + })); it('with overlapping loads from navigation and the preloader', fakeAsync(() => { - const preloader = TestBed.inject(RouterPreloader); - const router = TestBed.inject(Router); - router.events.subscribe(e => { - if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { - events.push(e); - } - }); + const preloader = TestBed.inject(RouterPreloader); + const router = TestBed.inject(Router); + router.events.subscribe((e) => { + if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { + events.push(e); + } + }); - lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1)); - subLoadChildrenSpy.and.returnValue(of(LoadedModule2).pipe(delay(5))); - preloader.preload().subscribe((x) => {}); - tick(); + lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1)); + subLoadChildrenSpy.and.returnValue(of(LoadedModule2).pipe(delay(5))); + preloader.preload().subscribe((x) => {}); + tick(); - // Load the out modules at start of test and ensure it and only - // it is loaded - delayLoadUnPaused.next(['lazymodule']); - tick(); - expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); - expect(events.map(e => e.toString())).toEqual([ - 'RouteConfigLoadStart(path: lazy)', - 'RouteConfigLoadEnd(path: lazy)', - ]); + // Load the out modules at start of test and ensure it and only + // it is loaded + delayLoadUnPaused.next(['lazymodule']); + tick(); + expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); + expect(events.map((e) => e.toString())).toEqual([ + 'RouteConfigLoadStart(path: lazy)', + 'RouteConfigLoadEnd(path: lazy)', + ]); - // Cause the load from router to start (has 5 tick delay) - router.navigateByUrl('/lazy/sub/LoadedModule2'); - tick(); // T1 - // Cause the load from preloader to start - delayLoadUnPaused.next(['lazymodule', 'submodule']); - tick(); // T2 + // Cause the load from router to start (has 5 tick delay) + router.navigateByUrl('/lazy/sub/LoadedModule2'); + tick(); // T1 + // Cause the load from preloader to start + delayLoadUnPaused.next(['lazymodule', 'submodule']); + tick(); // T2 - expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); - expect(subLoadChildrenSpy).toHaveBeenCalledTimes(1); - tick(5); // T2 to T7 enough time for mutiple loads to finish + expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); + expect(subLoadChildrenSpy).toHaveBeenCalledTimes(1); + tick(5); // T2 to T7 enough time for mutiple loads to finish - expect(subLoadChildrenSpy).toHaveBeenCalledTimes(1); - expect(events.map(e => e.toString())).toEqual([ - 'RouteConfigLoadStart(path: lazy)', 'RouteConfigLoadEnd(path: lazy)', - 'RouteConfigLoadStart(path: sub)', 'RouteConfigLoadEnd(path: sub)' - ]); - })); + expect(subLoadChildrenSpy).toHaveBeenCalledTimes(1); + expect(events.map((e) => e.toString())).toEqual([ + 'RouteConfigLoadStart(path: lazy)', + 'RouteConfigLoadEnd(path: lazy)', + 'RouteConfigLoadStart(path: sub)', + 'RouteConfigLoadEnd(path: sub)', + ]); + })); it('cope with factory fail from broken modules', fakeAsync(() => { - const preloader = TestBed.inject(RouterPreloader); - const router = TestBed.inject(Router); - router.events.subscribe(e => { - if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { - events.push(e); - } - }); + const preloader = TestBed.inject(RouterPreloader); + const router = TestBed.inject(Router); + router.events.subscribe((e) => { + if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { + events.push(e); + } + }); - class BrokenModuleFactory extends NgModuleFactory { - override moduleType: Type = LoadedModule1; - constructor() { - super(); - } - override create(_parentInjector: Injector|null): NgModuleRef { - throw 'Error: Broken module'; - } - } + class BrokenModuleFactory extends NgModuleFactory { + override moduleType: Type = LoadedModule1; + constructor() { + super(); + } + override create(_parentInjector: Injector | null): NgModuleRef { + throw 'Error: Broken module'; + } + } - lazyLoadChildrenSpy.and.returnValue(of(new BrokenModuleFactory())); - preloader.preload().subscribe((x) => {}); - tick(); - expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(0); + lazyLoadChildrenSpy.and.returnValue(of(new BrokenModuleFactory())); + preloader.preload().subscribe((x) => {}); + tick(); + expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(0); - router.navigateByUrl('/lazy/LoadedModule1').catch((reason) => { - expect(reason).toEqual('Error: Broken module'); - }); - tick(); + router.navigateByUrl('/lazy/LoadedModule1').catch((reason) => { + expect(reason).toEqual('Error: Broken module'); + }); + tick(); - expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); - lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1)); - router.navigateByUrl('/lazy/LoadedModule1').catch(() => { - fail('navigation should not throw'); - }); - tick(); + expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(1); + lazyLoadChildrenSpy.and.returnValue(of(LoadedModule1)); + router.navigateByUrl('/lazy/LoadedModule1').catch(() => { + fail('navigation should not throw'); + }); + tick(); - expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(2); - expect(subLoadChildrenSpy).toHaveBeenCalledTimes(0); - expect(events.map(e => e.toString())).toEqual([ - 'RouteConfigLoadStart(path: lazy)', 'RouteConfigLoadEnd(path: lazy)', - 'RouteConfigLoadStart(path: lazy)', 'RouteConfigLoadEnd(path: lazy)' - ]); - })); + expect(lazyLoadChildrenSpy).toHaveBeenCalledTimes(2); + expect(subLoadChildrenSpy).toHaveBeenCalledTimes(0); + expect(events.map((e) => e.toString())).toEqual([ + 'RouteConfigLoadStart(path: lazy)', + 'RouteConfigLoadEnd(path: lazy)', + 'RouteConfigLoadStart(path: lazy)', + 'RouteConfigLoadEnd(path: lazy)', + ]); + })); }); describe('should ignore errors', () => { @NgModule({ declarations: [LazyLoadedCmp], - imports: [RouterModule.forChild([{path: 'LoadedModule1', component: LazyLoadedCmp}])] + imports: [RouterModule.forChild([{path: 'LoadedModule1', component: LazyLoadedCmp}])], }) - class LoadedModule { - } + class LoadedModule {} beforeEach(() => { TestBed.configureTestingModule({ @@ -577,94 +617,96 @@ describe('RouterPreloader', () => { multi: true, useValue: [ {path: 'lazy1', loadChildren: jasmine.createSpy('expected1')}, - {path: 'lazy2', loadChildren: () => LoadedModule} - ] + {path: 'lazy2', loadChildren: () => LoadedModule}, + ], }, {provide: PreloadingStrategy, useExisting: PreloadAllModules}, - ] + ], }); }); + it('should work', fakeAsync( + inject([RouterPreloader, Router], (preloader: RouterPreloader, router: Router) => { + preloader.preload().subscribe(() => {}); - it('should work', - fakeAsync(inject([RouterPreloader, Router], (preloader: RouterPreloader, router: Router) => { - preloader.preload().subscribe(() => {}); + tick(); - tick(); - - const c = router.config; - expect(getLoadedRoutes(c[0])).not.toBeDefined(); - expect(getLoadedRoutes(c[1])).toBeDefined(); - }))); + const c = router.config; + expect(getLoadedRoutes(c[0])).not.toBeDefined(); + expect(getLoadedRoutes(c[1])).toBeDefined(); + }), + )); }); describe('should copy loaded configs', () => { const configs = [{path: 'LoadedModule1', component: LazyLoadedCmp}]; @NgModule({ declarations: [LazyLoadedCmp], - providers: [{provide: ROUTES, multi: true, useValue: configs}] + providers: [{provide: ROUTES, multi: true, useValue: configs}], }) - class LoadedModule { - } + class LoadedModule {} beforeEach(() => { TestBed.configureTestingModule({ providers: [ provideLocationMocks(), provideRouter( - [{path: 'lazy1', loadChildren: () => LoadedModule}], - withPreloading(PreloadAllModules)), + [{path: 'lazy1', loadChildren: () => LoadedModule}], + withPreloading(PreloadAllModules), + ), ], }); }); + it('should work', fakeAsync( + inject([RouterPreloader, Router], (preloader: RouterPreloader, router: Router) => { + preloader.preload().subscribe(() => {}); - it('should work', - fakeAsync(inject([RouterPreloader, Router], (preloader: RouterPreloader, router: Router) => { - preloader.preload().subscribe(() => {}); + tick(); - tick(); - - const c = router.config; - expect(getLoadedRoutes(c[0])).toBeDefined(); - expect(getLoadedRoutes(c[0])).not.toBe(configs); - expect(getLoadedRoutes(c[0])![0]).not.toBe(configs[0]); - expect(getLoadedRoutes(c[0])![0].component).toBe(configs[0].component); - }))); + const c = router.config; + expect(getLoadedRoutes(c[0])).toBeDefined(); + expect(getLoadedRoutes(c[0])).not.toBe(configs); + expect(getLoadedRoutes(c[0])![0]).not.toBe(configs[0]); + expect(getLoadedRoutes(c[0])![0].component).toBe(configs[0].component); + }), + )); }); - describe( - 'should work with lazy loaded modules that don\'t provide RouterModule.forChild()', () => { - @NgModule({ - declarations: [LazyLoadedCmp], - providers: [{ - provide: ROUTES, - multi: true, - useValue: [{path: 'LoadedModule1', component: LazyLoadedCmp}] - }] - }) - class LoadedModule { - } + describe("should work with lazy loaded modules that don't provide RouterModule.forChild()", () => { + @NgModule({ + declarations: [LazyLoadedCmp], + providers: [ + { + provide: ROUTES, + multi: true, + useValue: [{path: 'LoadedModule1', component: LazyLoadedCmp}], + }, + ], + }) + class LoadedModule {} - @NgModule({}) - class EmptyModule { - } + @NgModule({}) + class EmptyModule {} - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - provideLocationMocks(), - provideRouter( - [{path: 'lazyEmptyModule', loadChildren: () => EmptyModule}], - withPreloading(PreloadAllModules)), - ] - }); - }); - - it('should work', fakeAsync(inject([RouterPreloader], (preloader: RouterPreloader) => { - preloader.preload().subscribe(); - }))); + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideLocationMocks(), + provideRouter( + [{path: 'lazyEmptyModule', loadChildren: () => EmptyModule}], + withPreloading(PreloadAllModules), + ), + ], }); + }); + + it('should work', fakeAsync( + inject([RouterPreloader], (preloader: RouterPreloader) => { + preloader.preload().subscribe(); + }), + )); + }); describe('should preload loadComponent configs', () => { let lazyComponentSpy: jasmine.Spy; @@ -674,173 +716,176 @@ describe('RouterPreloader', () => { providers: [ provideLocationMocks(), provideRouter( - [{path: 'lazy', loadComponent: lazyComponentSpy}], withPreloading(PreloadAllModules)), - ] + [{path: 'lazy', loadComponent: lazyComponentSpy}], + withPreloading(PreloadAllModules), + ), + ], }); }); it('base case', fakeAsync(() => { - @Component({template: '', standalone: true}) - class LoadedComponent { - } + @Component({template: '', standalone: true}) + class LoadedComponent {} - const preloader = TestBed.inject(RouterPreloader); - lazyComponentSpy.and.returnValue(LoadedComponent); - preloader.preload().subscribe(() => {}); + const preloader = TestBed.inject(RouterPreloader); + lazyComponentSpy.and.returnValue(LoadedComponent); + preloader.preload().subscribe(() => {}); - tick(); + tick(); - const component = getLoadedComponent(TestBed.inject(Router).config[0]); - expect(component).toEqual(LoadedComponent); - })); + const component = getLoadedComponent(TestBed.inject(Router).config[0]); + expect(component).toEqual(LoadedComponent); + })); it('throws error when loadComponent is not standalone', fakeAsync(() => { - @Component({template: '', standalone: false}) - class LoadedComponent { - } - @Injectable({providedIn: 'root'}) - class ErrorTrackingPreloadAllModules implements PreloadingStrategy { - errors: Error[] = []; - preload(route: Route, fn: () => Observable): Observable { - return fn().pipe(catchError((e: Error) => { - this.errors.push(e); - return of(null); - })); - } - } + @Component({template: '', standalone: false}) + class LoadedComponent {} + @Injectable({providedIn: 'root'}) + class ErrorTrackingPreloadAllModules implements PreloadingStrategy { + errors: Error[] = []; + preload(route: Route, fn: () => Observable): Observable { + return fn().pipe( + catchError((e: Error) => { + this.errors.push(e); + return of(null); + }), + ); + } + } - TestBed.overrideProvider( - PreloadingStrategy, {useFactory: () => new ErrorTrackingPreloadAllModules()}); - const preloader = TestBed.inject(RouterPreloader); - lazyComponentSpy.and.returnValue(LoadedComponent); - preloader.preload().subscribe(() => {}); + TestBed.overrideProvider(PreloadingStrategy, { + useFactory: () => new ErrorTrackingPreloadAllModules(), + }); + const preloader = TestBed.inject(RouterPreloader); + lazyComponentSpy.and.returnValue(LoadedComponent); + preloader.preload().subscribe(() => {}); - tick(); - const strategy = TestBed.inject(PreloadingStrategy) as ErrorTrackingPreloadAllModules; - expect(strategy.errors[0]?.message).toMatch(/.*lazy.*must be standalone/); - })); + tick(); + const strategy = TestBed.inject(PreloadingStrategy) as ErrorTrackingPreloadAllModules; + expect(strategy.errors[0]?.message).toMatch(/.*lazy.*must be standalone/); + })); it('should recover from errors', fakeAsync(() => { - @Component({template: '', standalone: true}) - class LoadedComponent { - } + @Component({template: '', standalone: true}) + class LoadedComponent {} - const preloader = TestBed.inject(RouterPreloader); - lazyComponentSpy.and.returnValue(throwError('error loading chunk')); - preloader.preload().subscribe(() => {}); + const preloader = TestBed.inject(RouterPreloader); + lazyComponentSpy.and.returnValue(throwError('error loading chunk')); + preloader.preload().subscribe(() => {}); - tick(); + tick(); - const router = TestBed.inject(Router); - const c = router.config; - expect(lazyComponentSpy.calls.count()).toBe(1); - expect(getLoadedComponent(c[0])).not.toBeDefined(); + const router = TestBed.inject(Router); + const c = router.config; + expect(lazyComponentSpy.calls.count()).toBe(1); + expect(getLoadedComponent(c[0])).not.toBeDefined(); - lazyComponentSpy.and.returnValue(LoadedComponent); - router.navigateByUrl('/lazy'); - tick(); - expect(lazyComponentSpy.calls.count()).toBe(2); - expect(getLoadedComponent(c[0])).toBeDefined(); - })); + lazyComponentSpy.and.returnValue(LoadedComponent); + router.navigateByUrl('/lazy'); + tick(); + expect(lazyComponentSpy.calls.count()).toBe(2); + expect(getLoadedComponent(c[0])).toBeDefined(); + })); it('works when there is both loadComponent and loadChildren', fakeAsync(() => { - @Component({template: '', standalone: true}) - class LoadedComponent { - } + @Component({template: '', standalone: true}) + class LoadedComponent {} - @NgModule({ - providers: [ - provideLocationMocks(), - provideRouter([{path: 'child', component: LoadedComponent}]), - ], - }) - class LoadedModule { - } + @NgModule({ + providers: [ + provideLocationMocks(), + provideRouter([{path: 'child', component: LoadedComponent}]), + ], + }) + class LoadedModule {} - const router = TestBed.inject(Router); - router.config[0].loadChildren = () => LoadedModule; + const router = TestBed.inject(Router); + router.config[0].loadChildren = () => LoadedModule; - const preloader = TestBed.inject(RouterPreloader); - lazyComponentSpy.and.returnValue(LoadedComponent); - preloader.preload().subscribe(() => {}); + const preloader = TestBed.inject(RouterPreloader); + lazyComponentSpy.and.returnValue(LoadedComponent); + preloader.preload().subscribe(() => {}); - tick(); + tick(); - const component = getLoadedComponent(router.config[0]); - expect(component).toEqual(LoadedComponent); + const component = getLoadedComponent(router.config[0]); + expect(component).toEqual(LoadedComponent); - const childRoutes = getLoadedRoutes(router.config[0]); - expect(childRoutes).toBeDefined(); - expect(childRoutes![0].path).toEqual('child'); - })); + const childRoutes = getLoadedRoutes(router.config[0]); + expect(childRoutes).toBeDefined(); + expect(childRoutes![0].path).toEqual('child'); + })); it('loadComponent does not block loadChildren', fakeAsync(() => { - @Component({template: '', standalone: true}) - class LoadedComponent { - } + @Component({template: '', standalone: true}) + class LoadedComponent {} - lazyComponentSpy.and.returnValue(of(LoadedComponent).pipe(delay(5))); + lazyComponentSpy.and.returnValue(of(LoadedComponent).pipe(delay(5))); - @NgModule({ - providers: [ - provideLocationMocks(), - provideRouter([{ - path: 'child', - loadChildren: () => of([ - {path: 'grandchild', children: []}, - ]).pipe(delay(1)), - }]), - ] - }) - class LoadedModule { - } + @NgModule({ + providers: [ + provideLocationMocks(), + provideRouter([ + { + path: 'child', + loadChildren: () => of([{path: 'grandchild', children: []}]).pipe(delay(1)), + }, + ]), + ], + }) + class LoadedModule {} - const router = TestBed.inject(Router); - const baseRoute = router.config[0]; - baseRoute.loadChildren = () => of(LoadedModule).pipe(delay(1)); + const router = TestBed.inject(Router); + const baseRoute = router.config[0]; + baseRoute.loadChildren = () => of(LoadedModule).pipe(delay(1)); - const preloader = TestBed.inject(RouterPreloader); - preloader.preload().subscribe(() => {}); + const preloader = TestBed.inject(RouterPreloader); + preloader.preload().subscribe(() => {}); - tick(1); - // Loading should have started but not completed yet - expect(getLoadedComponent(baseRoute)).not.toBeDefined(); - const childRoutes = getLoadedRoutes(baseRoute); - expect(childRoutes).toBeDefined(); - // Loading should have started but not completed yet - expect(getLoadedRoutes(childRoutes![0])).not.toBeDefined(); + tick(1); + // Loading should have started but not completed yet + expect(getLoadedComponent(baseRoute)).not.toBeDefined(); + const childRoutes = getLoadedRoutes(baseRoute); + expect(childRoutes).toBeDefined(); + // Loading should have started but not completed yet + expect(getLoadedRoutes(childRoutes![0])).not.toBeDefined(); - tick(1); - // Loading should have started but not completed yet - expect(getLoadedComponent(baseRoute)).not.toBeDefined(); - expect(getLoadedRoutes(childRoutes![0])).toBeDefined(); + tick(1); + // Loading should have started but not completed yet + expect(getLoadedComponent(baseRoute)).not.toBeDefined(); + expect(getLoadedRoutes(childRoutes![0])).toBeDefined(); - tick(3); - expect(getLoadedComponent(baseRoute)).toBeDefined(); - })); + tick(3); + expect(getLoadedComponent(baseRoute)).toBeDefined(); + })); it('loads nested components', () => { @Component({template: '', standalone: true}) - class LoadedComponent { - } + class LoadedComponent {} lazyComponentSpy.and.returnValue(LoadedComponent); TestBed.inject(Router).resetConfig([ { path: 'a', loadComponent: lazyComponentSpy, - children: [{ - path: 'b', - loadComponent: lazyComponentSpy, - children: [{ - path: 'c', + children: [ + { + path: 'b', loadComponent: lazyComponentSpy, - children: [{ - path: 'd', - loadComponent: lazyComponentSpy, - }] - }] - }] + children: [ + { + path: 'c', + loadComponent: lazyComponentSpy, + children: [ + { + path: 'd', + loadComponent: lazyComponentSpy, + }, + ], + }, + ], + }, + ], }, ]); diff --git a/packages/router/test/router_scroller.spec.ts b/packages/router/test/router_scroller.spec.ts index 356c7f1470a..a015127eda1 100644 --- a/packages/router/test/router_scroller.spec.ts +++ b/packages/router/test/router_scroller.spec.ts @@ -17,7 +17,7 @@ import {RouterScroller} from '../src/router_scroller'; // TODO: add tests that exercise the `withInMemoryScrolling` feature of the provideRouter function const fakeZone = { runOutsideAngular: (fn: any) => fn(), - run: (fn: any) => fn() + run: (fn: any) => fn(), }; describe('RouterScroller', () => { it('defaults to disabled', () => { @@ -25,29 +25,42 @@ describe('RouterScroller', () => { const router = { events, parseUrl: (url: any) => new DefaultUrlSerializer().parse(url), - triggerEvent: (e: any) => events.next(e) + triggerEvent: (e: any) => events.next(e), }; - const viewportScroller = jasmine.createSpyObj( - 'viewportScroller', - ['getScrollPosition', 'scrollToPosition', 'scrollToAnchor', 'setHistoryScrollRestoration']); + const viewportScroller = jasmine.createSpyObj('viewportScroller', [ + 'getScrollPosition', + 'scrollToPosition', + 'scrollToAnchor', + 'setHistoryScrollRestoration', + ]); setScroll(viewportScroller, 0, 0); const scroller = new RouterScroller( - new DefaultUrlSerializer(), {events} as any, viewportScroller, fakeZone as any); + new DefaultUrlSerializer(), + {events} as any, + viewportScroller, + fakeZone as any, + ); expect((scroller as any).options.scrollPositionRestoration).toBe('disabled'); expect((scroller as any).options.anchorScrolling).toBe('disabled'); }); function nextScrollEvent(events: Subject): Promise { - return events.pipe(filter((e): e is Scroll => e instanceof Scroll), take(1)).toPromise() as - Promise; + return events + .pipe( + filter((e): e is Scroll => e instanceof Scroll), + take(1), + ) + .toPromise() as Promise; } describe('scroll to top', () => { it('should scroll to the top', async () => { - const {events, viewportScroller} = - createRouterScroller({scrollPositionRestoration: 'top', anchorScrolling: 'disabled'}); + const {events, viewportScroller} = createRouterScroller({ + scrollPositionRestoration: 'top', + anchorScrolling: 'disabled', + }); events.next(new NavigationStart(1, '/a')); events.next(new NavigationEnd(1, '/a', '/a')); @@ -68,8 +81,10 @@ describe('RouterScroller', () => { describe('scroll to the stored position', () => { it('should scroll to the stored position on popstate', async () => { - const {events, viewportScroller} = - createRouterScroller({scrollPositionRestoration: 'enabled', anchorScrolling: 'disabled'}); + const {events, viewportScroller} = createRouterScroller({ + scrollPositionRestoration: 'enabled', + anchorScrolling: 'disabled', + }); events.next(new NavigationStart(1, '/a')); events.next(new NavigationEnd(1, '/a', '/a')); @@ -92,8 +107,10 @@ describe('RouterScroller', () => { describe('anchor scrolling', () => { it('should work (scrollPositionRestoration is disabled)', async () => { - const {events, viewportScroller} = - createRouterScroller({scrollPositionRestoration: 'disabled', anchorScrolling: 'enabled'}); + const {events, viewportScroller} = createRouterScroller({ + scrollPositionRestoration: 'disabled', + anchorScrolling: 'enabled', + }); events.next(new NavigationStart(1, '/a#anchor')); events.next(new NavigationEnd(1, '/a#anchor', '/a#anchor')); await nextScrollEvent(events); @@ -114,8 +131,10 @@ describe('RouterScroller', () => { }); it('should work (scrollPositionRestoration is enabled)', async () => { - const {events, viewportScroller} = - createRouterScroller({scrollPositionRestoration: 'enabled', anchorScrolling: 'enabled'}); + const {events, viewportScroller} = createRouterScroller({ + scrollPositionRestoration: 'enabled', + anchorScrolling: 'enabled', + }); events.next(new NavigationStart(1, '/a#anchor')); events.next(new NavigationEnd(1, '/a#anchor', '/a#anchor')); await nextScrollEvent(events); @@ -138,68 +157,82 @@ describe('RouterScroller', () => { describe('extending a scroll service', () => { it('work', fakeAsync(() => { - const {events, viewportScroller} = createRouterScroller( - {scrollPositionRestoration: 'disabled', anchorScrolling: 'disabled'}); + const {events, viewportScroller} = createRouterScroller({ + scrollPositionRestoration: 'disabled', + anchorScrolling: 'disabled', + }); - events - .pipe(filter((e): e is Scroll => e instanceof Scroll && !!e.position), switchMap(p => { - // can be any delay (e.g., we can wait for NgRx store to emit an event) - const r = new Subject(); - setTimeout(() => { - r.next(p); - r.complete(); - }, 1000); - return r; - })) - .subscribe((e: Scroll) => { - viewportScroller.scrollToPosition(e.position); - }); + events + .pipe( + filter((e): e is Scroll => e instanceof Scroll && !!e.position), + switchMap((p) => { + // can be any delay (e.g., we can wait for NgRx store to emit an event) + const r = new Subject(); + setTimeout(() => { + r.next(p); + r.complete(); + }, 1000); + return r; + }), + ) + .subscribe((e: Scroll) => { + viewportScroller.scrollToPosition(e.position); + }); - events.next(new NavigationStart(1, '/a')); - events.next(new NavigationEnd(1, '/a', '/a')); - tick(); - setScroll(viewportScroller, 10, 100); + events.next(new NavigationStart(1, '/a')); + events.next(new NavigationEnd(1, '/a', '/a')); + tick(); + setScroll(viewportScroller, 10, 100); - events.next(new NavigationStart(2, '/b')); - events.next(new NavigationEnd(2, '/b', '/b')); - tick(); - setScroll(viewportScroller, 20, 200); + events.next(new NavigationStart(2, '/b')); + events.next(new NavigationEnd(2, '/b', '/b')); + tick(); + setScroll(viewportScroller, 20, 200); - events.next(new NavigationStart(3, '/c')); - events.next(new NavigationEnd(3, '/c', '/c')); - tick(); - setScroll(viewportScroller, 30, 300); + events.next(new NavigationStart(3, '/c')); + events.next(new NavigationEnd(3, '/c', '/c')); + tick(); + setScroll(viewportScroller, 30, 300); - events.next(new NavigationStart(4, '/a', 'popstate', {navigationId: 1})); - events.next(new NavigationEnd(4, '/a', '/a')); + events.next(new NavigationStart(4, '/a', 'popstate', {navigationId: 1})); + events.next(new NavigationEnd(4, '/a', '/a')); - tick(500); - expect(viewportScroller.scrollToPosition).not.toHaveBeenCalled(); + tick(500); + expect(viewportScroller.scrollToPosition).not.toHaveBeenCalled(); - events.next(new NavigationStart(5, '/a', 'popstate', {navigationId: 1})); - events.next(new NavigationEnd(5, '/a', '/a')); + events.next(new NavigationStart(5, '/a', 'popstate', {navigationId: 1})); + events.next(new NavigationEnd(5, '/a', '/a')); - tick(5000); - expect(viewportScroller.scrollToPosition).toHaveBeenCalledWith([10, 100]); - })); + tick(5000); + expect(viewportScroller.scrollToPosition).toHaveBeenCalledWith([10, 100]); + })); }); - - function createRouterScroller({scrollPositionRestoration, anchorScrolling}: { - scrollPositionRestoration: 'disabled'|'enabled'|'top', - anchorScrolling: 'disabled'|'enabled' + function createRouterScroller({ + scrollPositionRestoration, + anchorScrolling, + }: { + scrollPositionRestoration: 'disabled' | 'enabled' | 'top'; + anchorScrolling: 'disabled' | 'enabled'; }) { const events = new Subject(); const transitions: any = {events}; - const viewportScroller = jasmine.createSpyObj( - 'viewportScroller', - ['getScrollPosition', 'scrollToPosition', 'scrollToAnchor', 'setHistoryScrollRestoration']); + const viewportScroller = jasmine.createSpyObj('viewportScroller', [ + 'getScrollPosition', + 'scrollToPosition', + 'scrollToAnchor', + 'setHistoryScrollRestoration', + ]); setScroll(viewportScroller, 0, 0); const scroller = new RouterScroller( - new DefaultUrlSerializer(), transitions, viewportScroller, fakeZone as any, - {scrollPositionRestoration, anchorScrolling}); + new DefaultUrlSerializer(), + transitions, + viewportScroller, + fakeZone as any, + {scrollPositionRestoration, anchorScrolling}, + ); scroller.init(); return {events, viewportScroller}; diff --git a/packages/router/test/router_state.spec.ts b/packages/router/test/router_state.spec.ts index c4ee3f64609..f0adcd960bf 100644 --- a/packages/router/test/router_state.spec.ts +++ b/packages/router/test/router_state.spec.ts @@ -8,7 +8,14 @@ import {BehaviorSubject} from 'rxjs'; -import {ActivatedRoute, ActivatedRouteSnapshot, advanceActivatedRoute, equalParamsAndUrlSegments, RouterState, RouterStateSnapshot} from '../src/router_state'; +import { + ActivatedRoute, + ActivatedRouteSnapshot, + advanceActivatedRoute, + equalParamsAndUrlSegments, + RouterState, + RouterStateSnapshot, +} from '../src/router_state'; import {Params, RouteTitleKey} from '../src/shared'; import {UrlSegment} from '../src/url_tree'; import {TreeNode} from '../src/utils/tree'; @@ -105,14 +112,27 @@ describe('RouterState & Snapshot', () => { describe('equalParamsAndUrlSegments', () => { function createSnapshot(params: Params, url: UrlSegment[]): ActivatedRouteSnapshot { const snapshot = new (ActivatedRouteSnapshot as any)( - url, params, null, null, null, null, null, null, null, -1, null); + url, + params, + null, + null, + null, + null, + null, + null, + null, + -1, + null, + ); snapshot._routerState = new (RouterStateSnapshot as any)('', new TreeNode(snapshot, [])); return snapshot; } function createSnapshotPairWithParent( - params: [Params, Params], parentParams: [Params, Params], - urls: [string, string]): [ActivatedRouteSnapshot, ActivatedRouteSnapshot] { + params: [Params, Params], + parentParams: [Params, Params], + urls: [string, string], + ): [ActivatedRouteSnapshot, ActivatedRouteSnapshot] { const snapshot1 = createSnapshot(params[0], []); const snapshot2 = createSnapshot(params[1], []); @@ -120,49 +140,67 @@ describe('RouterState & Snapshot', () => { const snapshot2Parent = createSnapshot(parentParams[1], [new UrlSegment(urls[1], {})]); (snapshot1 as any)._routerState = new (RouterStateSnapshot as any)( - '', new TreeNode(snapshot1Parent, [new TreeNode(snapshot1, [])])); + '', + new TreeNode(snapshot1Parent, [new TreeNode(snapshot1, [])]), + ); (snapshot2 as any)._routerState = new (RouterStateSnapshot as any)( - '', new TreeNode(snapshot2Parent, [new TreeNode(snapshot2, [])])); + '', + new TreeNode(snapshot2Parent, [new TreeNode(snapshot2, [])]), + ); return [snapshot1, snapshot2]; } it('should return false when params are different', () => { - expect(equalParamsAndUrlSegments(createSnapshot({a: '1'}, []), createSnapshot({a: '2'}, []))) - .toEqual(false); + expect( + equalParamsAndUrlSegments(createSnapshot({a: '1'}, []), createSnapshot({a: '2'}, [])), + ).toEqual(false); }); it('should return false when urls are different', () => { - expect(equalParamsAndUrlSegments( - createSnapshot({a: '1'}, [new UrlSegment('a', {})]), - createSnapshot({a: '1'}, [new UrlSegment('b', {})]))) - .toEqual(false); + expect( + equalParamsAndUrlSegments( + createSnapshot({a: '1'}, [new UrlSegment('a', {})]), + createSnapshot({a: '1'}, [new UrlSegment('b', {})]), + ), + ).toEqual(false); }); it('should return true othewise', () => { - expect(equalParamsAndUrlSegments( - createSnapshot({a: '1'}, [new UrlSegment('a', {})]), - createSnapshot({a: '1'}, [new UrlSegment('a', {})]))) - .toEqual(true); + expect( + equalParamsAndUrlSegments( + createSnapshot({a: '1'}, [new UrlSegment('a', {})]), + createSnapshot({a: '1'}, [new UrlSegment('a', {})]), + ), + ).toEqual(true); }); it('should return false when upstream params are different', () => { - const [snapshot1, snapshot2] = - createSnapshotPairWithParent([{a: '1'}, {a: '1'}], [{b: '1'}, {c: '1'}], ['a', 'a']); + const [snapshot1, snapshot2] = createSnapshotPairWithParent( + [{a: '1'}, {a: '1'}], + [{b: '1'}, {c: '1'}], + ['a', 'a'], + ); expect(equalParamsAndUrlSegments(snapshot1, snapshot2)).toEqual(false); }); it('should return false when upstream urls are different', () => { - const [snapshot1, snapshot2] = - createSnapshotPairWithParent([{a: '1'}, {a: '1'}], [{b: '1'}, {b: '1'}], ['a', 'b']); + const [snapshot1, snapshot2] = createSnapshotPairWithParent( + [{a: '1'}, {a: '1'}], + [{b: '1'}, {b: '1'}], + ['a', 'b'], + ); expect(equalParamsAndUrlSegments(snapshot1, snapshot2)).toEqual(false); }); it('should return true when upstream urls and params are equal', () => { - const [snapshot1, snapshot2] = - createSnapshotPairWithParent([{a: '1'}, {a: '1'}], [{b: '1'}, {b: '1'}], ['a', 'a']); + const [snapshot1, snapshot2] = createSnapshotPairWithParent( + [{a: '1'}, {a: '1'}], + [{b: '1'}, {b: '1'}], + ['a', 'a'], + ); expect(equalParamsAndUrlSegments(snapshot1, snapshot2)).toEqual(true); }); @@ -180,7 +218,18 @@ describe('RouterState & Snapshot', () => { const fragment = ''; const data = {}; const snapshot = new (ActivatedRouteSnapshot as any)( - url, params, queryParams, fragment, data, null, null, null, null, -1, null); + url, + params, + queryParams, + fragment, + data, + null, + null, + null, + null, + -1, + null, + ); const state = new (RouterStateSnapshot as any)('', new TreeNode(snapshot, [])); snapshot._routerState = state; return snapshot; @@ -206,12 +255,23 @@ describe('RouterState & Snapshot', () => { const data = {[RouteTitleKey]: 'resolved title'}; const route = createActivatedRoute('a'); const snapshot = new (ActivatedRouteSnapshot as any)( - [], null, null, null, data, null, 'test', null, null, -1, null!); - let resolvedTitle: string|undefined; + [], + null, + null, + null, + data, + null, + 'test', + null, + null, + -1, + null!, + ); + let resolvedTitle: string | undefined; route.data.next(data); - route.title.forEach((title: string|undefined) => { + route.title.forEach((title: string | undefined) => { resolvedTitle = title; }); @@ -223,11 +283,29 @@ describe('RouterState & Snapshot', () => { function createActivatedRouteSnapshot(cmp: string) { return new (ActivatedRouteSnapshot as any)( - [], null, null, null, null, null, cmp, null, null, -1, null!); + [], + null, + null, + null, + null, + null, + cmp, + null, + null, + -1, + null!, + ); } function createActivatedRoute(cmp: string) { return new (ActivatedRoute as any)( - new BehaviorSubject([new UrlSegment('', {})]), new BehaviorSubject({}), null, null, - new BehaviorSubject({}), null, cmp, null); + new BehaviorSubject([new UrlSegment('', {})]), + new BehaviorSubject({}), + null, + null, + new BehaviorSubject({}), + null, + cmp, + null, + ); } diff --git a/packages/router/test/shared.spec.ts b/packages/router/test/shared.spec.ts index 1dfaf1826c5..2243413c0c9 100644 --- a/packages/router/test/shared.spec.ts +++ b/packages/router/test/shared.spec.ts @@ -43,13 +43,12 @@ describe('ParamsMap', () => { expect(map.getAll('name')).toEqual([]); }); - it('should not error when trying to call ParamMap.get function using an object created with Object.create() function', - () => { - const objectToMap: Params = Object.create(null); - objectToMap['single'] = 's'; - objectToMap['multiple'] = ['m1', 'm2']; - const paramMaps: ParamMap = convertToParamMap(objectToMap); - expect(() => paramMaps.get('single')).not.toThrow(); - expect(paramMaps.get('single')).toEqual('s'); - }); + it('should not error when trying to call ParamMap.get function using an object created with Object.create() function', () => { + const objectToMap: Params = Object.create(null); + objectToMap['single'] = 's'; + objectToMap['multiple'] = ['m1', 'm2']; + const paramMaps: ParamMap = convertToParamMap(objectToMap); + expect(() => paramMaps.get('single')).not.toThrow(); + expect(paramMaps.get('single')).toEqual('s'); + }); }); diff --git a/packages/router/test/standalone.spec.ts b/packages/router/test/standalone.spec.ts index 46b30146983..3ec65e5fe85 100644 --- a/packages/router/test/standalone.spec.ts +++ b/packages/router/test/standalone.spec.ts @@ -12,354 +12,375 @@ import {By} from '@angular/platform-browser'; import {provideRoutes, Router, RouterModule, ROUTES} from '@angular/router'; @Component({template: '
simple standalone
', standalone: true}) -export class SimpleStandaloneComponent { -} +export class SimpleStandaloneComponent {} @Component({template: '
not standalone
', standalone: false}) -export class NotStandaloneComponent { -} +export class NotStandaloneComponent {} @Component({ template: '', standalone: true, imports: [RouterModule], }) -export class RootCmp { -} +export class RootCmp {} describe('standalone in Router API', () => { describe('loadChildren => routes', () => { it('can navigate to and render standalone component', fakeAsync(() => { - TestBed.configureTestingModule({ - imports: [RouterModule.forRoot([ - { - path: 'lazy', - component: RootCmp, - loadChildren: () => [{path: '', component: SimpleStandaloneComponent}], - }, - ])] - }); + TestBed.configureTestingModule({ + imports: [ + RouterModule.forRoot([ + { + path: 'lazy', + component: RootCmp, + loadChildren: () => [{path: '', component: SimpleStandaloneComponent}], + }, + ]), + ], + }); - const root = TestBed.createComponent(RootCmp); + const root = TestBed.createComponent(RootCmp); - const router = TestBed.inject(Router); - router.navigateByUrl('/lazy'); - advance(root); - expect(root.nativeElement.innerHTML).toContain('simple standalone'); - })); + const router = TestBed.inject(Router); + router.navigateByUrl('/lazy'); + advance(root); + expect(root.nativeElement.innerHTML).toContain('simple standalone'); + })); - it('throws an error when loadChildren=>routes has a component that is not standalone', - fakeAsync(() => { - TestBed.configureTestingModule({ - imports: [RouterModule.forRoot([ - { - path: 'lazy', - component: RootCmp, - loadChildren: () => [{path: 'notstandalone', component: NotStandaloneComponent}], - }, - ])] - }); + it('throws an error when loadChildren=>routes has a component that is not standalone', fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [ + RouterModule.forRoot([ + { + path: 'lazy', + component: RootCmp, + loadChildren: () => [{path: 'notstandalone', component: NotStandaloneComponent}], + }, + ]), + ], + }); - const root = TestBed.createComponent(RootCmp); + const root = TestBed.createComponent(RootCmp); - const router = TestBed.inject(Router); - router.navigateByUrl('/lazy/notstandalone'); - expect(() => advance(root)) - .toThrowError(/.*lazy\/notstandalone.*component must be standalone/); - })); + const router = TestBed.inject(Router); + router.navigateByUrl('/lazy/notstandalone'); + expect(() => advance(root)).toThrowError( + /.*lazy\/notstandalone.*component must be standalone/, + ); + })); }); describe('route providers', () => { it('can provide a guard on a route', fakeAsync(() => { - @Injectable() - class ConfigurableGuard { - static canActivateValue = false; - canActivate() { - return ConfigurableGuard.canActivateValue; - } - } + @Injectable() + class ConfigurableGuard { + static canActivateValue = false; + canActivate() { + return ConfigurableGuard.canActivateValue; + } + } - TestBed.configureTestingModule({ - imports: [ - RouterModule.forRoot([{ - path: 'simple', - providers: [ConfigurableGuard], - canActivate: [ConfigurableGuard], - component: SimpleStandaloneComponent - }]), - ], - }); - const root = TestBed.createComponent(RootCmp); + TestBed.configureTestingModule({ + imports: [ + RouterModule.forRoot([ + { + path: 'simple', + providers: [ConfigurableGuard], + canActivate: [ConfigurableGuard], + component: SimpleStandaloneComponent, + }, + ]), + ], + }); + const root = TestBed.createComponent(RootCmp); - ConfigurableGuard.canActivateValue = false; - const router = TestBed.inject(Router); - router.navigateByUrl('/simple'); - advance(root); - expect(root.nativeElement.innerHTML).not.toContain('simple standalone'); - expect(router.url).not.toContain('simple'); + ConfigurableGuard.canActivateValue = false; + const router = TestBed.inject(Router); + router.navigateByUrl('/simple'); + advance(root); + expect(root.nativeElement.innerHTML).not.toContain('simple standalone'); + expect(router.url).not.toContain('simple'); - ConfigurableGuard.canActivateValue = true; - router.navigateByUrl('/simple'); - advance(root); - expect(root.nativeElement.innerHTML).toContain('simple standalone'); - expect(router.url).toContain('simple'); - })); + ConfigurableGuard.canActivateValue = true; + router.navigateByUrl('/simple'); + advance(root); + expect(root.nativeElement.innerHTML).toContain('simple standalone'); + expect(router.url).toContain('simple'); + })); it('can inject provider on a route into component', fakeAsync(() => { - @Injectable() - class Service { - value = 'my service'; - } + @Injectable() + class Service { + value = 'my service'; + } - @Component({template: `{{service.value}}`}) - class MyComponent { - constructor(readonly service: Service) {} - } + @Component({template: `{{service.value}}`}) + class MyComponent { + constructor(readonly service: Service) {} + } - TestBed.configureTestingModule({ - imports: [ - RouterModule.forRoot([{path: 'home', providers: [Service], component: MyComponent}]), - ], - declarations: [MyComponent], - }); - const root = TestBed.createComponent(RootCmp); + TestBed.configureTestingModule({ + imports: [ + RouterModule.forRoot([{path: 'home', providers: [Service], component: MyComponent}]), + ], + declarations: [MyComponent], + }); + const root = TestBed.createComponent(RootCmp); - const router = TestBed.inject(Router); - router.navigateByUrl('/home'); - advance(root); - expect(root.nativeElement.innerHTML).toContain('my service'); - expect(router.url).toContain('home'); - })); + const router = TestBed.inject(Router); + router.navigateByUrl('/home'); + advance(root); + expect(root.nativeElement.innerHTML).toContain('my service'); + expect(router.url).toContain('home'); + })); - it('can not inject provider in lazy loaded ngModule from component on same level', - fakeAsync(() => { - @Injectable() - class Service { - value = 'my service'; - } + it('can not inject provider in lazy loaded ngModule from component on same level', fakeAsync(() => { + @Injectable() + class Service { + value = 'my service'; + } - @NgModule({providers: [Service]}) - class LazyModule { - } + @NgModule({providers: [Service]}) + class LazyModule {} - @Component({template: `{{service.value}}`}) - class MyComponent { - constructor(readonly service: Service) {} - } + @Component({template: `{{service.value}}`}) + class MyComponent { + constructor(readonly service: Service) {} + } - TestBed.configureTestingModule({ - imports: [ - RouterModule.forRoot( - [{path: 'home', loadChildren: () => LazyModule, component: MyComponent}]), - ], - declarations: [MyComponent], - }); - const root = TestBed.createComponent(RootCmp); + TestBed.configureTestingModule({ + imports: [ + RouterModule.forRoot([ + {path: 'home', loadChildren: () => LazyModule, component: MyComponent}, + ]), + ], + declarations: [MyComponent], + }); + const root = TestBed.createComponent(RootCmp); - const router = TestBed.inject(Router); - router.navigateByUrl('/home'); - expect(() => advance(root)).toThrowError(); - })); + const router = TestBed.inject(Router); + router.navigateByUrl('/home'); + expect(() => advance(root)).toThrowError(); + })); it('component from lazy module can inject provider from parent route', fakeAsync(() => { - @Injectable() - class Service { - value = 'my service'; - } + @Injectable() + class Service { + value = 'my service'; + } - @Component({template: `{{service.value}}`}) - class MyComponent { - constructor(readonly service: Service) {} - } - @NgModule({ - providers: [Service], - declarations: [MyComponent], - imports: [RouterModule.forChild([{path: '', component: MyComponent}])] - }) - class LazyModule { - } + @Component({template: `{{service.value}}`}) + class MyComponent { + constructor(readonly service: Service) {} + } + @NgModule({ + providers: [Service], + declarations: [MyComponent], + imports: [RouterModule.forChild([{path: '', component: MyComponent}])], + }) + class LazyModule {} + TestBed.configureTestingModule({ + imports: [RouterModule.forRoot([{path: 'home', loadChildren: () => LazyModule}])], + }); + const root = TestBed.createComponent(RootCmp); - TestBed.configureTestingModule({ - imports: [ - RouterModule.forRoot([{path: 'home', loadChildren: () => LazyModule}]), - ], - }); - const root = TestBed.createComponent(RootCmp); + const router = TestBed.inject(Router); + router.navigateByUrl('/home'); + advance(root); + expect(root.nativeElement.innerHTML).toContain('my service'); + })); - const router = TestBed.inject(Router); - router.navigateByUrl('/home'); - advance(root); - expect(root.nativeElement.innerHTML).toContain('my service'); - })); + it('gets the correct injector for guards and components when combining lazy modules and route providers', fakeAsync(() => { + const canActivateLog: string[] = []; + abstract class ServiceBase { + abstract name: string; + canActivate() { + canActivateLog.push(this.name); + return true; + } + } - it('gets the correct injector for guards and components when combining lazy modules and route providers', - fakeAsync(() => { - const canActivateLog: string[] = []; - abstract class ServiceBase { - abstract name: string; - canActivate() { - canActivateLog.push(this.name); - return true; - } - } + @Injectable() + class Service1 extends ServiceBase { + override name = 'service1'; + } - @Injectable() - class Service1 extends ServiceBase { - override name = 'service1'; - } + @Injectable() + class Service2 extends ServiceBase { + override name = 'service2'; + } - @Injectable() - class Service2 extends ServiceBase { - override name = 'service2'; - } + @Injectable() + class Service3 extends ServiceBase { + override name = 'service3'; + } - @Injectable() - class Service3 extends ServiceBase { - override name = 'service3'; - } + @Component({template: `parent`}) + class ParentCmp { + constructor(readonly service: ServiceBase) {} + } + @Component({template: `child`}) + class ChildCmp { + constructor(readonly service: ServiceBase) {} + } - @Component({template: `parent`}) - class ParentCmp { - constructor(readonly service: ServiceBase) {} - } - @Component({template: `child`}) - class ChildCmp { - constructor(readonly service: ServiceBase) {} - } + @Component({template: `child2`}) + class ChildCmp2 { + constructor(readonly service: ServiceBase) {} + } + @NgModule({ + providers: [{provide: ServiceBase, useClass: Service2}], + declarations: [ChildCmp, ChildCmp2], + imports: [ + RouterModule.forChild([ + { + path: '', + // This component and guard should get Service2 since it's provided in this module + component: ChildCmp, + canActivate: [ServiceBase], + }, + { + path: 'child2', + providers: [{provide: ServiceBase, useFactory: () => new Service3()}], + // This component and guard should get Service3 since it's provided on this route + component: ChildCmp2, + canActivate: [ServiceBase], + }, + ]), + ], + }) + class LazyModule {} - @Component({template: `child2`}) - class ChildCmp2 { - constructor(readonly service: ServiceBase) {} - } - @NgModule({ - providers: [{provide: ServiceBase, useClass: Service2}], - declarations: [ChildCmp, ChildCmp2], - imports: [RouterModule.forChild([ - { - path: '', - // This component and guard should get Service2 since it's provided in this module - component: ChildCmp, - canActivate: [ServiceBase], - }, - { - path: 'child2', - providers: [{provide: ServiceBase, useFactory: () => new Service3()}], - // This component and guard should get Service3 since it's provided on this route - component: ChildCmp2, - canActivate: [ServiceBase], - }, - ])] - }) - class LazyModule { - } + TestBed.configureTestingModule({ + imports: [ + RouterModule.forRoot([ + { + path: 'home', + // This component and guard should get Service1 since it's provided on this route + component: ParentCmp, + canActivate: [ServiceBase], + providers: [{provide: ServiceBase, useFactory: () => new Service1()}], + loadChildren: () => LazyModule, + }, + ]), + ], + declarations: [ParentCmp], + }); + const root = TestBed.createComponent(RootCmp); + const router = TestBed.inject(Router); + router.navigateByUrl('/home'); + advance(root); + expect(canActivateLog).toEqual(['service1', 'service2']); + expect( + root.debugElement.query(By.directive(ParentCmp)).componentInstance.service.name, + ).toEqual('service1'); + expect( + root.debugElement.query(By.directive(ChildCmp)).componentInstance.service.name, + ).toEqual('service2'); - TestBed.configureTestingModule({ - imports: [ - RouterModule.forRoot([{ - path: 'home', - // This component and guard should get Service1 since it's provided on this route - component: ParentCmp, - canActivate: [ServiceBase], - providers: [{provide: ServiceBase, useFactory: () => new Service1()}], - loadChildren: () => LazyModule - }]), - ], - declarations: [ParentCmp], - }); - const root = TestBed.createComponent(RootCmp); - - const router = TestBed.inject(Router); - router.navigateByUrl('/home'); - advance(root); - expect(canActivateLog).toEqual(['service1', 'service2']); - expect(root.debugElement.query(By.directive(ParentCmp)).componentInstance.service.name) - .toEqual('service1'); - expect(root.debugElement.query(By.directive(ChildCmp)).componentInstance.service.name) - .toEqual('service2'); - - router.navigateByUrl('/home/child2'); - advance(root); - expect(canActivateLog).toEqual(['service1', 'service2', 'service3']); - expect(root.debugElement.query(By.directive(ChildCmp2)).componentInstance.service.name) - .toEqual('service3'); - })); + router.navigateByUrl('/home/child2'); + advance(root); + expect(canActivateLog).toEqual(['service1', 'service2', 'service3']); + expect( + root.debugElement.query(By.directive(ChildCmp2)).componentInstance.service.name, + ).toEqual('service3'); + })); }); describe('loadComponent', () => { it('does not load component when canActivate returns false', fakeAsync(() => { - const loadComponentSpy = jasmine.createSpy(); - @Injectable({providedIn: 'root'}) - class Guard { - canActivate() { - return false; - } - } + const loadComponentSpy = jasmine.createSpy(); + @Injectable({providedIn: 'root'}) + class Guard { + canActivate() { + return false; + } + } - TestBed.configureTestingModule({ - imports: [RouterModule.forRoot([{ - path: 'home', - loadComponent: loadComponentSpy, - canActivate: [Guard], - }])] - }); + TestBed.configureTestingModule({ + imports: [ + RouterModule.forRoot([ + { + path: 'home', + loadComponent: loadComponentSpy, + canActivate: [Guard], + }, + ]), + ], + }); - TestBed.inject(Router).navigateByUrl('/home'); - tick(); - expect(loadComponentSpy).not.toHaveBeenCalled(); - })); + TestBed.inject(Router).navigateByUrl('/home'); + tick(); + expect(loadComponentSpy).not.toHaveBeenCalled(); + })); it('loads and renders lazy component', fakeAsync(() => { - TestBed.configureTestingModule({ - imports: [RouterModule.forRoot([{ - path: 'home', - loadComponent: () => SimpleStandaloneComponent, - }])], - }); + TestBed.configureTestingModule({ + imports: [ + RouterModule.forRoot([ + { + path: 'home', + loadComponent: () => SimpleStandaloneComponent, + }, + ]), + ], + }); - const root = TestBed.createComponent(RootCmp); - TestBed.inject(Router).navigateByUrl('/home'); - advance(root); - expect(root.nativeElement.innerHTML).toContain('simple standalone'); - })); + const root = TestBed.createComponent(RootCmp); + TestBed.inject(Router).navigateByUrl('/home'); + advance(root); + expect(root.nativeElement.innerHTML).toContain('simple standalone'); + })); it('throws error when loadComponent is not standalone', fakeAsync(() => { - TestBed.configureTestingModule({ - imports: [RouterModule.forRoot([{ - path: 'home', - loadComponent: () => NotStandaloneComponent, - }])], - }); + TestBed.configureTestingModule({ + imports: [ + RouterModule.forRoot([ + { + path: 'home', + loadComponent: () => NotStandaloneComponent, + }, + ]), + ], + }); - const root = TestBed.createComponent(RootCmp); - TestBed.inject(Router).navigateByUrl('/home'); - expect(() => advance(root)).toThrowError(/.*home.*component must be standalone/); - })); + const root = TestBed.createComponent(RootCmp); + TestBed.inject(Router).navigateByUrl('/home'); + expect(() => advance(root)).toThrowError(/.*home.*component must be standalone/); + })); it('throws error when loadComponent is used with a module', fakeAsync(() => { - @NgModule() - class LazyModule { - } + @NgModule() + class LazyModule {} - TestBed.configureTestingModule({ - imports: [RouterModule.forRoot([{ - path: 'home', - loadComponent: () => LazyModule, - }])], - }); + TestBed.configureTestingModule({ + imports: [ + RouterModule.forRoot([ + { + path: 'home', + loadComponent: () => LazyModule, + }, + ]), + ], + }); - const root = TestBed.createComponent(RootCmp); - TestBed.inject(Router).navigateByUrl('/home'); - expect(() => advance(root)).toThrowError(/.*home.*Use 'loadChildren' instead/); - })); + const root = TestBed.createComponent(RootCmp); + TestBed.inject(Router).navigateByUrl('/home'); + expect(() => advance(root)).toThrowError(/.*home.*Use 'loadChildren' instead/); + })); }); describe('default export unwrapping', () => { it('should work for loadComponent', async () => { TestBed.configureTestingModule({ - imports: [RouterModule.forRoot([{ - path: 'home', - loadComponent: () => import('./default_export_component'), - }])], + imports: [ + RouterModule.forRoot([ + { + path: 'home', + loadComponent: () => import('./default_export_component'), + }, + ]), + ], }); const root = TestBed.createComponent(RootCmp); @@ -371,10 +392,14 @@ describe('standalone in Router API', () => { it('should work for loadChildren', async () => { TestBed.configureTestingModule({ - imports: [RouterModule.forRoot([{ - path: 'home', - loadChildren: () => import('./default_export_routes'), - }])], + imports: [ + RouterModule.forRoot([ + { + path: 'home', + loadChildren: () => import('./default_export_routes'), + }, + ]), + ], }); const root = TestBed.createComponent(RootCmp); @@ -387,13 +412,12 @@ describe('standalone in Router API', () => { }); describe('provideRoutes', () => { - it('warns if provideRoutes is used without provideRouter, RouterModule, or RouterModule.forRoot', - () => { - spyOn(console, 'warn'); - TestBed.configureTestingModule({providers: [provideRoutes([])]}); - TestBed.inject(ROUTES); - expect(console.warn).toHaveBeenCalled(); - }); + it('warns if provideRoutes is used without provideRouter, RouterModule, or RouterModule.forRoot', () => { + spyOn(console, 'warn'); + TestBed.configureTestingModule({providers: [provideRoutes([])]}); + TestBed.inject(ROUTES); + expect(console.warn).toHaveBeenCalled(); + }); }); function advance(fixture: ComponentFixture) { diff --git a/packages/router/test/url_serializer.spec.ts b/packages/router/test/url_serializer.spec.ts index 34decffbec8..0e0ec524663 100644 --- a/packages/router/test/url_serializer.spec.ts +++ b/packages/router/test/url_serializer.spec.ts @@ -7,7 +7,14 @@ */ import {PRIMARY_OUTLET} from '../src/shared'; -import {DefaultUrlSerializer, encodeUriFragment, encodeUriQuery, encodeUriSegment, serializePath, UrlSegmentGroup} from '../src/url_tree'; +import { + DefaultUrlSerializer, + encodeUriFragment, + encodeUriQuery, + encodeUriSegment, + serializePath, + UrlSegmentGroup, +} from '../src/url_tree'; describe('url serializer', () => { const url = new DefaultUrlSerializer(); @@ -60,8 +67,9 @@ describe('url serializer', () => { const tree = url.parse('/path/to/something;query=file=test;query2=test2'); expect(tree.root.children['primary'].segments[2].path).toEqual('something'); expect(tree.root.children['primary'].segments[2].parameterMap.keys).toHaveSize(2); - expect(tree.root.children['primary'].segments[2].parameterMap.get('query')) - .toEqual('file=test'); + expect(tree.root.children['primary'].segments[2].parameterMap.get('query')).toEqual( + 'file=test', + ); expect(tree.root.children['primary'].segments[2].parameterMap.get('query2')).toEqual('test2'); }); @@ -86,8 +94,9 @@ describe('url serializer', () => { }); it('should not parse empty path segments with params', () => { - expect(() => url.parse('/one/two/(;a=1//right:;b=2)')) - .toThrowError(/Empty path url segment cannot have parameters/); + expect(() => url.parse('/one/two/(;a=1//right:;b=2)')).toThrowError( + /Empty path url segment cannot have parameters/, + ); }); it('should parse scoped secondary segments', () => { @@ -225,19 +234,23 @@ describe('url serializer', () => { describe('encoding/decoding', () => { it('should encode/decode path segments and parameters', () => { - const u = `/${encodeUriSegment('one two')};${encodeUriSegment('p 1')}=${ - encodeUriSegment('v 1')};${encodeUriSegment('p 2')}=${encodeUriSegment('v 2')}`; + const u = `/${encodeUriSegment('one two')};${encodeUriSegment('p 1')}=${encodeUriSegment( + 'v 1', + )};${encodeUriSegment('p 2')}=${encodeUriSegment('v 2')}`; const tree = url.parse(u); expect(tree.root.children[PRIMARY_OUTLET].segments[0].path).toEqual('one two'); - expect(tree.root.children[PRIMARY_OUTLET].segments[0].parameters) - .toEqual({['p 1']: 'v 1', ['p 2']: 'v 2'}); + expect(tree.root.children[PRIMARY_OUTLET].segments[0].parameters).toEqual({ + ['p 1']: 'v 1', + ['p 2']: 'v 2', + }); expect(url.serialize(tree)).toEqual(u); }); it('should encode/decode "slash" in path segments and parameters', () => { - const u = `/${encodeUriSegment('one/two')};${encodeUriSegment('p/1')}=${ - encodeUriSegment('v/1')}/three`; + const u = `/${encodeUriSegment('one/two')};${encodeUriSegment('p/1')}=${encodeUriSegment( + 'v/1', + )}/three`; const tree = url.parse(u); const segment = tree.root.children[PRIMARY_OUTLET].segments[0]; expect(segment.path).toEqual('one/two'); @@ -248,8 +261,9 @@ describe('url serializer', () => { }); it('should encode/decode query params', () => { - const u = `/one?${encodeUriQuery('p 1')}=${encodeUriQuery('v 1')}&${encodeUriQuery('p 2')}=${ - encodeUriQuery('v 2')}`; + const u = `/one?${encodeUriQuery('p 1')}=${encodeUriQuery('v 1')}&${encodeUriQuery( + 'p 2', + )}=${encodeUriQuery('v 2')}`; const tree = url.parse(u); expect(tree.queryParams).toEqual({'p 1': 'v 1', 'p 2': 'v 2'}); @@ -277,7 +291,7 @@ describe('url serializer', () => { it('should encode query params leaving sub-delimiters intact', () => { const percentChars = '/?#&+=[] '; const percentCharsEncoded = '%2F%3F%23%26%2B%3D%5B%5D%20'; - const intactChars = '!$\'()*,;:'; + const intactChars = "!$'()*,;:"; const params = percentChars + intactChars; const paramsEncoded = percentCharsEncoded + intactChars; const mixedCaseString = 'sTrInG'; @@ -308,7 +322,6 @@ describe('url serializer', () => { const auxParsed = url.parse(auxRoutesUrl).root; const fooParsed = url.parse(fooValueUrl).root; - // Test base case expect(auxParsed.children[PRIMARY_OUTLET].segments.length).toBe(1); expect(auxParsed.children[PRIMARY_OUTLET].segments[0].path).toBe('abc'); @@ -320,7 +333,7 @@ describe('url serializer', () => { expect(fooParsed.children[PRIMARY_OUTLET].segments.length).toBe(1); expect(fooParsed.children[PRIMARY_OUTLET].segments[0].path).toBe('abc'); expect(fooParsed.children[PRIMARY_OUTLET].segments[0].parameters).toEqual({ - foo: '(other:val)' + foo: '(other:val)', }); }); @@ -371,18 +384,17 @@ describe('url serializer', () => { expect(url.serialize(parsed)).toBe(`/foo#${notEncoded}${encoded}`); }); - it('should encode minimal special characters plus parens and semi-colon in matrix params', - () => { - const notEncoded = unreserved + `:@!$'*,&`; - const encode = ` /%=#()[];?+`; - const encoded = `%20%2F%25%3D%23%28%29%5B%5D%3B%3F%2B`; + it('should encode minimal special characters plus parens and semi-colon in matrix params', () => { + const notEncoded = unreserved + `:@!$'*,&`; + const encode = ` /%=#()[];?+`; + const encoded = `%20%2F%25%3D%23%28%29%5B%5D%3B%3F%2B`; - const parsed = url.parse('/foo'); + const parsed = url.parse('/foo'); - parsed.root.children[PRIMARY_OUTLET].segments[0].parameters = {notEncoded, encode}; + parsed.root.children[PRIMARY_OUTLET].segments[0].parameters = {notEncoded, encode}; - expect(url.serialize(parsed)).toBe(`/foo;notEncoded=${notEncoded};encode=${encoded}`); - }); + expect(url.serialize(parsed)).toBe(`/foo;notEncoded=${notEncoded};encode=${encoded}`); + }); it('should encode special characters in the path the same as matrix params', () => { const notEncoded = unreserved + `:@!$'*,&`; @@ -417,11 +429,14 @@ describe('url serializer', () => { }); function expectSegment( - segment: UrlSegmentGroup, expected: string, hasChildren: boolean = false): void { - if (segment.segments.filter(s => s.path === '').length > 0) { + segment: UrlSegmentGroup, + expected: string, + hasChildren: boolean = false, +): void { + if (segment.segments.filter((s) => s.path === '').length > 0) { throw new Error(`UrlSegments cannot be empty ${segment.segments}`); } - const p = segment.segments.map(p => serializePath(p)).join('/'); + const p = segment.segments.map((p) => serializePath(p)).join('/'); expect(p).toEqual(expected); expect(Object.keys(segment.children).length > 0).toEqual(hasChildren); } diff --git a/packages/router/test/url_tree.spec.ts b/packages/router/test/url_tree.spec.ts index 33c07357a0c..f7b8b7c02ad 100644 --- a/packages/router/test/url_tree.spec.ts +++ b/packages/router/test/url_tree.spec.ts @@ -254,13 +254,15 @@ describe('UrlTree', () => { expect(containsTree(t1, t2, {...subsetMatchOptions, matrixParams})).toBe(true); }); - it('should return true when matrix params match on subset of urlTree match ' + - 'with container paths split into multiple segments', - () => { - const t1 = serializer.parse('/one;a=1/(two;b=2//left:three)'); - const t2 = serializer.parse('/one;a=1/two;b=2'); - expect(containsTree(t1, t2, {...subsetMatchOptions, matrixParams})).toBe(true); - }); + it( + 'should return true when matrix params match on subset of urlTree match ' + + 'with container paths split into multiple segments', + () => { + const t1 = serializer.parse('/one;a=1/(two;b=2//left:three)'); + const t2 = serializer.parse('/one;a=1/two;b=2'); + expect(containsTree(t1, t2, {...subsetMatchOptions, matrixParams})).toBe(true); + }, + ); }); describe('subset match', () => { @@ -290,13 +292,15 @@ describe('UrlTree', () => { expect(containsTree(t1, t2, {...subsetMatchOptions, matrixParams})).toBe(true); }); - it('should return true when matrix params match on subset of urlTree match ' + - 'with container paths split into multiple segments', - () => { - const t1 = serializer.parse('/one;a=1/(two;b=2//left:three)'); - const t2 = serializer.parse('/one;a=1/two'); - expect(containsTree(t1, t2, {...subsetMatchOptions, matrixParams})).toBe(true); - }); + it( + 'should return true when matrix params match on subset of urlTree match ' + + 'with container paths split into multiple segments', + () => { + const t1 = serializer.parse('/one;a=1/(two;b=2//left:three)'); + const t2 = serializer.parse('/one;a=1/two'); + expect(containsTree(t1, t2, {...subsetMatchOptions, matrixParams})).toBe(true); + }, + ); }); }); }); diff --git a/packages/router/test/utils/tree.spec.ts b/packages/router/test/utils/tree.spec.ts index f1fb80d016e..bccc1353633 100644 --- a/packages/router/test/utils/tree.spec.ts +++ b/packages/router/test/utils/tree.spec.ts @@ -21,8 +21,9 @@ describe('tree', () => { }); it('should return the parent of a node (second child)', () => { - const t = new Tree(new TreeNode( - 1, [new TreeNode(2, []), new TreeNode(3, [])])) as any; + const t = new Tree( + new TreeNode(1, [new TreeNode(2, []), new TreeNode(3, [])]), + ) as any; expect(t.parent(1)).toEqual(null); expect(t.parent(3)).toEqual(1); }); @@ -40,8 +41,9 @@ describe('tree', () => { }); it('should return the siblings of a node', () => { - const t = new Tree(new TreeNode( - 1, [new TreeNode(2, []), new TreeNode(3, [])])) as any; + const t = new Tree( + new TreeNode(1, [new TreeNode(2, []), new TreeNode(3, [])]), + ) as any; expect(t.siblings(2)).toEqual([3]); expect(t.siblings(1)).toEqual([]); }); diff --git a/packages/router/test/view_transitions.spec.ts b/packages/router/test/view_transitions.spec.ts index f6d2a9d08f2..d5fa75395f1 100644 --- a/packages/router/test/view_transitions.spec.ts +++ b/packages/router/test/view_transitions.spec.ts @@ -10,8 +10,14 @@ import {DOCUMENT} from '@angular/common'; import {Component, destroyPlatform, inject} from '@angular/core'; import {bootstrapApplication} from '@angular/platform-browser'; import {withBody} from '@angular/private/testing'; -import {Event, NavigationEnd, provideRouter, Router, withDisabledInitialNavigation, withViewTransitions} from '@angular/router'; - +import { + Event, + NavigationEnd, + provideRouter, + Router, + withDisabledInitialNavigation, + withViewTransitions, +} from '@angular/router'; describe('view transitions', () => { if (isNode) { @@ -27,16 +33,17 @@ describe('view transitions', () => { standalone: true, template: ``, }) - class App { - } + class App {} beforeEach(withBody('', () => {})); it('should skip initial transition', async () => { const appRef = await bootstrapApplication(App, { - providers: [provideRouter( + providers: [ + provideRouter( [{path: '**', component: App}], withDisabledInitialNavigation(), withViewTransitions({skipInitialTransition: true}), - )] + ), + ], }); const doc = appRef.injector.get(DOCUMENT); @@ -57,16 +64,14 @@ describe('view transitions', () => { template: `b`, standalone: true, }) - class ComponentB { - } + class ComponentB {} - - const res = await bootstrapApplication( - App, - {providers: [provideRouter([{path: 'b', component: ComponentB}], withViewTransitions())]}); + const res = await bootstrapApplication(App, { + providers: [provideRouter([{path: 'b', component: ComponentB}], withViewTransitions())], + }); const router = res.injector.get(Router); const eventLog = [] as Event[]; - router.events.subscribe(e => { + router.events.subscribe((e) => { eventLog.push(e); }); @@ -90,11 +95,13 @@ describe('view transitions', () => { const transitionSpy = jasmine.createSpy(); const appRef = await bootstrapApplication(App, { - providers: [provideRouter( + providers: [ + provideRouter( [{path: '**', component: App}], withDisabledInitialNavigation(), withViewTransitions({onViewTransitionCreated: transitionSpy}), - )] + ), + ], }); const doc = appRef.injector.get(DOCUMENT); diff --git a/packages/router/testing/src/router_testing_harness.ts b/packages/router/testing/src/router_testing_harness.ts index 2d4448568f6..99bf665a4f2 100644 --- a/packages/router/testing/src/router_testing_harness.ts +++ b/packages/router/testing/src/router_testing_harness.ts @@ -83,15 +83,15 @@ export class RouterTestingHarness { this.fixture.detectChanges(); } /** The `DebugElement` of the `RouterOutlet` component. `null` if the outlet is not activated. */ - get routeDebugElement(): DebugElement|null { + get routeDebugElement(): DebugElement | null { const outlet = (this.fixture.componentInstance as RootCmp).outlet; if (!outlet || !outlet.isActivated) { return null; } - return this.fixture.debugElement.query(v => v.componentInstance === outlet.component); + return this.fixture.debugElement.query((v) => v.componentInstance === outlet.component); } /** The native element of the `RouterOutlet` component. `null` if the outlet is not activated. */ - get routeNativeElement(): HTMLElement|null { + get routeNativeElement(): HTMLElement | null { return this.routeDebugElement?.nativeElement ?? null; } @@ -111,7 +111,7 @@ export class RouterTestingHarness { * @returns The activated component instance of the `RouterOutlet` after navigation completes * (`null` if the outlet does not get activated). */ - async navigateByUrl(url: string): Promise; + async navigateByUrl(url: string): Promise; /** * Triggers a router navigation and waits for it to complete. * @@ -133,10 +133,10 @@ export class RouterTestingHarness { * @returns The activated component instance of the `RouterOutlet` after navigation completes. */ async navigateByUrl(url: string, requiredRoutedComponentType: Type): Promise; - async navigateByUrl(url: string, requiredRoutedComponentType?: Type): Promise { + async navigateByUrl(url: string, requiredRoutedComponentType?: Type): Promise { const router = TestBed.inject(Router); let resolveFn!: () => void; - const redirectTrackingPromise = new Promise(resolve => { + const redirectTrackingPromise = new Promise((resolve) => { resolveFn = resolve; }); afterNextNavigation(TestBed.inject(Router), resolveFn); @@ -148,16 +148,20 @@ export class RouterTestingHarness { // rejects if (outlet && outlet.isActivated && outlet.activatedRoute.component) { const activatedComponent = outlet.component; - if (requiredRoutedComponentType !== undefined && - !(activatedComponent instanceof requiredRoutedComponentType)) { - throw new Error(`Unexpected routed component type. Expected ${ - requiredRoutedComponentType.name} but got ${activatedComponent.constructor.name}`); + if ( + requiredRoutedComponentType !== undefined && + !(activatedComponent instanceof requiredRoutedComponentType) + ) { + throw new Error( + `Unexpected routed component type. Expected ${requiredRoutedComponentType.name} but got ${activatedComponent.constructor.name}`, + ); } return activatedComponent as T; } else { if (requiredRoutedComponentType !== undefined) { - throw new Error(`Unexpected routed component type. Expected ${ - requiredRoutedComponentType.name} but the navigation did not activate any component.`); + throw new Error( + `Unexpected routed component type. Expected ${requiredRoutedComponentType.name} but the navigation did not activate any component.`, + ); } return null; } diff --git a/packages/router/testing/src/router_testing_module.ts b/packages/router/testing/src/router_testing_module.ts index db875c73710..2ac2630fdaf 100644 --- a/packages/router/testing/src/router_testing_module.ts +++ b/packages/router/testing/src/router_testing_module.ts @@ -9,10 +9,27 @@ import {Location} from '@angular/common'; import {provideLocationMocks} from '@angular/common/testing'; import {Compiler, inject, Injector, ModuleWithProviders, NgModule} from '@angular/core'; -import {ChildrenOutletContexts, ExtraOptions, NoPreloading, Route, Router, ROUTER_CONFIGURATION, RouteReuseStrategy, RouterModule, ROUTES, Routes, TitleStrategy, UrlHandlingStrategy, UrlSerializer, withPreloading, ɵROUTER_PROVIDERS as ROUTER_PROVIDERS} from '@angular/router'; +import { + ChildrenOutletContexts, + ExtraOptions, + NoPreloading, + Route, + Router, + ROUTER_CONFIGURATION, + RouteReuseStrategy, + RouterModule, + ROUTES, + Routes, + TitleStrategy, + UrlHandlingStrategy, + UrlSerializer, + withPreloading, + ɵROUTER_PROVIDERS as ROUTER_PROVIDERS, +} from '@angular/router'; -function isUrlHandlingStrategy(opts: ExtraOptions| - UrlHandlingStrategy): opts is UrlHandlingStrategy { +function isUrlHandlingStrategy( + opts: ExtraOptions | UrlHandlingStrategy, +): opts is UrlHandlingStrategy { // This property check is needed because UrlHandlingStrategy is an interface and doesn't exist at // runtime. return 'shouldProcessUrl' in opts; @@ -20,8 +37,9 @@ function isUrlHandlingStrategy(opts: ExtraOptions| function throwInvalidConfigError(parameter: string): never { throw new Error( - `Parameter ${parameter} does not match the one available in the injector. ` + - '`setupTestingRouter` is meant to be used as a factory function with dependencies coming from DI.'); + `Parameter ${parameter} does not match the one available in the injector. ` + + '`setupTestingRouter` is meant to be used as a factory function with dependencies coming from DI.', + ); } /** @@ -56,17 +74,19 @@ function throwInvalidConfigError(parameter: string): never { provideLocationMocks(), withPreloading(NoPreloading).ɵproviders, {provide: ROUTES, multi: true, useValue: []}, - ] + ], }) export class RouterTestingModule { - static withRoutes(routes: Routes, config?: ExtraOptions): - ModuleWithProviders { + static withRoutes( + routes: Routes, + config?: ExtraOptions, + ): ModuleWithProviders { return { ngModule: RouterTestingModule, providers: [ {provide: ROUTES, multi: true, useValue: routes}, {provide: ROUTER_CONFIGURATION, useValue: config ? config : {}}, - ] + ], }; } } diff --git a/packages/router/testing/test/router_testing_harness.spec.ts b/packages/router/testing/test/router_testing_harness.spec.ts index 8192255ef1d..9d2f977292a 100644 --- a/packages/router/testing/test/router_testing_harness.spec.ts +++ b/packages/router/testing/test/router_testing_harness.spec.ts @@ -39,14 +39,20 @@ describe('navigateForTest', () => { it('executes guards on the path', async () => { let guardCalled = false; TestBed.configureTestingModule({ - providers: [provideRouter([{ - path: '', - canActivate: [() => { - guardCalled = true; - return true; - }], - children: [] - }])] + providers: [ + provideRouter([ + { + path: '', + canActivate: [ + () => { + guardCalled = true; + return true; + }, + ], + children: [], + }, + ]), + ], }); await RouterTestingHarness.create('/'); expect(guardCalled).toBeTrue(); @@ -54,17 +60,22 @@ describe('navigateForTest', () => { it('throws error if routing throws', async () => { TestBed.configureTestingModule({ - providers: [provideRouter( + providers: [ + provideRouter( [ { path: 'e', - canActivate: [() => { - throw new Error('oh no'); - }], - children: [] + canActivate: [ + () => { + throw new Error('oh no'); + }, + ], + children: [], }, ], - withRouterConfig({resolveNavigationPromiseOnError: true}))] + withRouterConfig({resolveNavigationPromiseOnError: true}), + ), + ], }); const harness = await RouterTestingHarness.create(); await expectAsync(harness.navigateByUrl('e')).toBeResolvedTo(null); @@ -77,9 +88,7 @@ describe('navigateForTest', () => { } TestBed.configureTestingModule({ - providers: [ - provideRouter([{path: ':id', component: TestCmp}]), - ] + providers: [provideRouter([{path: ':id', component: TestCmp}])], }); const harness = await RouterTestingHarness.create(); const activatedComponent = await harness.navigateByUrl('/123', TestCmp); @@ -89,33 +98,25 @@ describe('navigateForTest', () => { expect(harness.routeNativeElement?.innerHTML).toContain('456'); }); - it('throws an error if the routed component instance does not match the one required', - async () => { - @Component({standalone: true, template: ''}) - class TestCmp { - } - @Component({standalone: true, template: ''}) - class OtherCmp { - } + it('throws an error if the routed component instance does not match the one required', async () => { + @Component({standalone: true, template: ''}) + class TestCmp {} + @Component({standalone: true, template: ''}) + class OtherCmp {} - TestBed.configureTestingModule({ - providers: [ - provideRouter([{path: '**', component: TestCmp}]), - ] - }); - const harness = await RouterTestingHarness.create(); - await expectAsync(harness.navigateByUrl('/123', OtherCmp)).toBeRejected(); - }); + TestBed.configureTestingModule({ + providers: [provideRouter([{path: '**', component: TestCmp}])], + }); + const harness = await RouterTestingHarness.create(); + await expectAsync(harness.navigateByUrl('/123', OtherCmp)).toBeRejected(); + }); it('throws an error if navigation fails but expected a component instance', async () => { @Component({standalone: true, template: ''}) - class TestCmp { - } + class TestCmp {} TestBed.configureTestingModule({ - providers: [ - provideRouter([{path: '**', canActivate: [() => false], component: TestCmp}]), - ] + providers: [provideRouter([{path: '**', canActivate: [() => false], component: TestCmp}])], }); const harness = await RouterTestingHarness.create(); await expectAsync(harness.navigateByUrl('/123', TestCmp)).toBeRejected(); @@ -123,11 +124,9 @@ describe('navigateForTest', () => { it('waits for redirects using router.navigate', async () => { @Component({standalone: true, template: 'test'}) - class TestCmp { - } + class TestCmp {} @Component({standalone: true, template: 'redirect'}) - class OtherCmp { - } + class OtherCmp {} TestBed.configureTestingModule({ providers: [ @@ -135,11 +134,11 @@ describe('navigateForTest', () => { { path: 'test', canActivate: [() => inject(Router).navigateByUrl('/redirect')], - component: TestCmp + component: TestCmp, }, {path: 'redirect', canActivate: [() => of(true).pipe(delay(100))], component: OtherCmp}, ]), - ] + ], }); await RouterTestingHarness.create('test'); expect(TestBed.inject(Router).url).toEqual('/redirect'); diff --git a/packages/router/upgrade/src/upgrade.ts b/packages/router/upgrade/src/upgrade.ts index 524a6d012a3..52267fd2841 100644 --- a/packages/router/upgrade/src/upgrade.ts +++ b/packages/router/upgrade/src/upgrade.ts @@ -38,7 +38,7 @@ export const RouterUpgradeInitializer = { provide: APP_BOOTSTRAP_LISTENER, multi: true, useFactory: locationSyncBootstrapListener as (ngUpgrade: UpgradeModule) => () => void, - deps: [UpgradeModule] + deps: [UpgradeModule], }; /** @@ -62,7 +62,7 @@ export function locationSyncBootstrapListener(ngUpgrade: UpgradeModule) { * * @publicApi */ -export function setUpLocationSync(ngUpgrade: UpgradeModule, urlType: 'path'|'hash' = 'path') { +export function setUpLocationSync(ngUpgrade: UpgradeModule, urlType: 'path' | 'hash' = 'path') { if (!ngUpgrade.$injector) { throw new Error(` RouterUpgradeInitializer can be used only after UpgradeModule.bootstrap has been called. @@ -73,36 +73,41 @@ export function setUpLocationSync(ngUpgrade: UpgradeModule, urlType: 'path'|'has const router: Router = ngUpgrade.injector.get(Router); const location: Location = ngUpgrade.injector.get(Location); - ngUpgrade.$injector.get('$rootScope') - .$on( - '$locationChangeStart', - (event: any, newUrl: string, oldUrl: string, - newState?: {[k: string]: unknown}|RestoredState, - oldState?: {[k: string]: unknown}|RestoredState) => { - // Navigations coming from Angular router have a navigationId state - // property. Don't trigger Angular router navigation again if it is - // caused by a URL change from the current Angular router - // navigation. - const currentNavigationId = router.getCurrentNavigation()?.id; - const newStateNavigationId = newState?.navigationId; - if (newStateNavigationId !== undefined && - newStateNavigationId === currentNavigationId) { - return; - } + ngUpgrade.$injector + .get('$rootScope') + .$on( + '$locationChangeStart', + ( + event: any, + newUrl: string, + oldUrl: string, + newState?: {[k: string]: unknown} | RestoredState, + oldState?: {[k: string]: unknown} | RestoredState, + ) => { + // Navigations coming from Angular router have a navigationId state + // property. Don't trigger Angular router navigation again if it is + // caused by a URL change from the current Angular router + // navigation. + const currentNavigationId = router.getCurrentNavigation()?.id; + const newStateNavigationId = newState?.navigationId; + if (newStateNavigationId !== undefined && newStateNavigationId === currentNavigationId) { + return; + } - let url; - if (urlType === 'path') { - url = resolveUrl(newUrl); - } else if (urlType === 'hash') { - // Remove the first hash from the URL - const hashIdx = newUrl.indexOf('#'); - url = resolveUrl(newUrl.substring(0, hashIdx) + newUrl.substring(hashIdx + 1)); - } else { - throw 'Invalid URLType passed to setUpLocationSync: ' + urlType; - } - const path = location.normalize(url.pathname); - router.navigateByUrl(path + url.search + url.hash); - }); + let url; + if (urlType === 'path') { + url = resolveUrl(newUrl); + } else if (urlType === 'hash') { + // Remove the first hash from the URL + const hashIdx = newUrl.indexOf('#'); + url = resolveUrl(newUrl.substring(0, hashIdx) + newUrl.substring(hashIdx + 1)); + } else { + throw 'Invalid URLType passed to setUpLocationSync: ' + urlType; + } + const path = location.normalize(url.pathname); + router.navigateByUrl(path + url.search + url.hash); + }, + ); } /** @@ -123,8 +128,8 @@ export function setUpLocationSync(ngUpgrade: UpgradeModule, urlType: 'path'|'has * https://github.com/angular/angular.js/blob/2c7400e7d07b0f6cec1817dab40b9250ce8ebce6/src/ng/urlUtils.js#L26-L33 * for more info. */ -let anchor: HTMLAnchorElement|undefined; -function resolveUrl(url: string): {pathname: string, search: string, hash: string} { +let anchor: HTMLAnchorElement | undefined; +function resolveUrl(url: string): {pathname: string; search: string; hash: string} { anchor ??= document.createElement('a'); anchor.setAttribute('href', url); @@ -134,6 +139,6 @@ function resolveUrl(url: string): {pathname: string, search: string, hash: strin // IE does not start `pathname` with `/` like other browsers. pathname: `/${anchor.pathname.replace(/^\//, '')}`, search: anchor.search, - hash: anchor.hash + hash: anchor.hash, }; } diff --git a/packages/router/upgrade/test/upgrade.spec.ts b/packages/router/upgrade/test/upgrade.spec.ts index fdc6675f237..de9e86fa90b 100644 --- a/packages/router/upgrade/test/upgrade.spec.ts +++ b/packages/router/upgrade/test/upgrade.spec.ts @@ -39,7 +39,7 @@ export class $rootScopeMock { $broadcast(evt: string, ...args: any[]) { if (this.events[evt]) { - this.events[evt].forEach(fn => { + this.events[evt].forEach((fn) => { fn.apply(fn, [/** angular.IAngularEvent*/ {}, ...args]); }); } @@ -47,7 +47,7 @@ export class $rootScopeMock { defaultPrevented: false, preventDefault() { this.defaultPrevented = true; - } + }, }; } @@ -61,7 +61,7 @@ export class $rootScopeMock { } $digest() { - this.watchers.forEach(fn => fn()); + this.watchers.forEach((fn) => fn()); } } @@ -73,7 +73,10 @@ describe('setUpLocationSync', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ - RouterModule.forRoot([{path: '1', children: []}, {path: '2', children: []}]), + RouterModule.forRoot([ + {path: '1', children: []}, + {path: '2', children: []}, + ]), UpgradeModule, LocationUpgradeTestModule.config(), ], @@ -96,16 +99,15 @@ describe('setUpLocationSync', () => { `); }); - it('should get the $rootScope from AngularJS and set an $on watch on $locationChangeStart', - () => { - const $rootScope = upgradeModule.$injector.get('$rootScope'); - spyOn($rootScope, '$on'); + it('should get the $rootScope from AngularJS and set an $on watch on $locationChangeStart', () => { + const $rootScope = upgradeModule.$injector.get('$rootScope'); + spyOn($rootScope, '$on'); - setUpLocationSync(upgradeModule); + setUpLocationSync(upgradeModule); - expect($rootScope.$on).toHaveBeenCalledTimes(1); - expect($rootScope.$on).toHaveBeenCalledWith('$locationChangeStart', jasmine.any(Function)); - }); + expect($rootScope.$on).toHaveBeenCalledTimes(1); + expect($rootScope.$on).toHaveBeenCalledWith('$locationChangeStart', jasmine.any(Function)); + }); it('should navigate by url every time $locationChangeStart is broadcasted', () => { const url = 'https://google.com'; @@ -168,36 +170,36 @@ describe('setUpLocationSync', () => { }); it('should not duplicate navigations triggered by Angular router', fakeAsync(() => { - spyOn(TestBed.inject(UrlCodec), 'parse').and.returnValue({ - pathname: '', - href: '', - protocol: '', - host: '', - search: '', - hash: '', - hostname: '', - port: '', - }); - const $rootScope = upgradeModule.$injector.get('$rootScope'); - spyOn($rootScope, '$broadcast').and.callThrough(); - setUpLocationSync(upgradeModule); - // Inject location shim so its urlChangeListener subscribes - TestBed.inject($locationShim); + spyOn(TestBed.inject(UrlCodec), 'parse').and.returnValue({ + pathname: '', + href: '', + protocol: '', + host: '', + search: '', + hash: '', + hostname: '', + port: '', + }); + const $rootScope = upgradeModule.$injector.get('$rootScope'); + spyOn($rootScope, '$broadcast').and.callThrough(); + setUpLocationSync(upgradeModule); + // Inject location shim so its urlChangeListener subscribes + TestBed.inject($locationShim); - router.navigateByUrl('/1'); - location.normalize.and.returnValue('/1'); - flush(); - expect(router.navigateByUrl).toHaveBeenCalledTimes(1); - expect($rootScope.$broadcast.calls.argsFor(0)[0]).toEqual('$locationChangeStart'); - expect($rootScope.$broadcast.calls.argsFor(1)[0]).toEqual('$locationChangeSuccess'); - $rootScope.$broadcast.calls.reset(); - router.navigateByUrl.calls.reset(); + router.navigateByUrl('/1'); + location.normalize.and.returnValue('/1'); + flush(); + expect(router.navigateByUrl).toHaveBeenCalledTimes(1); + expect($rootScope.$broadcast.calls.argsFor(0)[0]).toEqual('$locationChangeStart'); + expect($rootScope.$broadcast.calls.argsFor(1)[0]).toEqual('$locationChangeSuccess'); + $rootScope.$broadcast.calls.reset(); + router.navigateByUrl.calls.reset(); - location.go('/2'); - location.normalize.and.returnValue('/2'); - flush(); - expect($rootScope.$broadcast.calls.argsFor(0)[0]).toEqual('$locationChangeStart'); - expect($rootScope.$broadcast.calls.argsFor(1)[0]).toEqual('$locationChangeSuccess'); - expect(router.navigateByUrl).toHaveBeenCalledTimes(1); - })); + location.go('/2'); + location.normalize.and.returnValue('/2'); + flush(); + expect($rootScope.$broadcast.calls.argsFor(0)[0]).toEqual('$locationChangeStart'); + expect($rootScope.$broadcast.calls.argsFor(1)[0]).toEqual('$locationChangeSuccess'); + expect(router.navigateByUrl).toHaveBeenCalledTimes(1); + })); }); diff --git a/packages/router/upgrade/test/upgrade_location_test_module.ts b/packages/router/upgrade/test/upgrade_location_test_module.ts index 956df8348b0..c7023a3abe4 100644 --- a/packages/router/upgrade/test/upgrade_location_test_module.ts +++ b/packages/router/upgrade/test/upgrade_location_test_module.ts @@ -6,9 +6,20 @@ * found in the LICENSE file at https://angular.io/license */ -import {APP_BASE_HREF, CommonModule, Location, LocationStrategy, PlatformLocation} from '@angular/common'; +import { + APP_BASE_HREF, + CommonModule, + Location, + LocationStrategy, + PlatformLocation, +} from '@angular/common'; import {MockPlatformLocation} from '@angular/common/testing'; -import {$locationShim, $locationShimProvider, LocationUpgradeModule, UrlCodec} from '@angular/common/upgrade'; +import { + $locationShim, + $locationShimProvider, + LocationUpgradeModule, + UrlCodec, +} from '@angular/common/upgrade'; import {Inject, InjectionToken, ModuleWithProviders, NgModule, Optional} from '@angular/core'; import {UpgradeModule} from '@angular/upgrade/static'; @@ -25,9 +36,9 @@ export interface LocationUpgradeTestingConfig { * * Is used in DI to configure the router. */ -export const LOC_UPGRADE_TEST_CONFIG = - new InjectionToken('LOC_UPGRADE_TEST_CONFIG'); - +export const LOC_UPGRADE_TEST_CONFIG = new InjectionToken( + 'LOC_UPGRADE_TEST_CONFIG', +); export const APP_BASE_HREF_RESOLVED = new InjectionToken('APP_BASE_HREF_RESOLVED'); @@ -36,12 +47,14 @@ export const APP_BASE_HREF_RESOLVED = new InjectionToken('APP_BASE_HREF_ */ @NgModule({imports: [CommonModule]}) export class LocationUpgradeTestModule { - static config(config?: LocationUpgradeTestingConfig): - ModuleWithProviders { + static config( + config?: LocationUpgradeTestingConfig, + ): ModuleWithProviders { return { ngModule: LocationUpgradeTestModule, providers: [ - {provide: LOC_UPGRADE_TEST_CONFIG, useValue: config || {}}, { + {provide: LOC_UPGRADE_TEST_CONFIG, useValue: config || {}}, + { provide: PlatformLocation, useFactory: (appBaseHref?: string) => { if (config && config.appBaseHref != null) { @@ -49,35 +62,49 @@ export class LocationUpgradeTestModule { } else if (appBaseHref == null) { appBaseHref = ''; } - return new MockPlatformLocation( - {startUrl: config && config.startUrl, appBaseHref: appBaseHref}); + return new MockPlatformLocation({ + startUrl: config && config.startUrl, + appBaseHref: appBaseHref, + }); }, - deps: [[new Inject(APP_BASE_HREF), new Optional()]] + deps: [[new Inject(APP_BASE_HREF), new Optional()]], }, { provide: $locationShim, useFactory: provide$location, deps: [ - UpgradeModule, Location, PlatformLocation, UrlCodec, LocationStrategy, - LOC_UPGRADE_TEST_CONFIG - ] + UpgradeModule, + Location, + PlatformLocation, + UrlCodec, + LocationStrategy, + LOC_UPGRADE_TEST_CONFIG, + ], }, - LocationUpgradeModule - .config({ - appBaseHref: config && config.appBaseHref, - useHash: config && config.useHash || false - }) - .providers! + LocationUpgradeModule.config({ + appBaseHref: config && config.appBaseHref, + useHash: (config && config.useHash) || false, + }).providers!, ], }; } } export function provide$location( - ngUpgrade: UpgradeModule, location: Location, platformLocation: PlatformLocation, - urlCodec: UrlCodec, locationStrategy: LocationStrategy, config?: LocationUpgradeTestingConfig) { - const $locationProvider = - new $locationShimProvider(ngUpgrade, location, platformLocation, urlCodec, locationStrategy); + ngUpgrade: UpgradeModule, + location: Location, + platformLocation: PlatformLocation, + urlCodec: UrlCodec, + locationStrategy: LocationStrategy, + config?: LocationUpgradeTestingConfig, +) { + const $locationProvider = new $locationShimProvider( + ngUpgrade, + location, + platformLocation, + urlCodec, + locationStrategy, + ); $locationProvider.hashPrefix(config && config.hashPrefix); $locationProvider.html5Mode(config && !config.useHash);