From 6c05c80f19bc84189cc1d2f2e029f6f13d60dc18 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Tue, 29 Dec 2020 14:41:42 -0800 Subject: [PATCH] feat(router): Add more find-tuned control in `routerLinkActiveOptions` (#40303) This commit adds more configurability to the `Router#isActive` method and `RouterLinkActive#routerLinkActiveOptions`. It allows tuning individual match options for query params and the url tree, which were either both partial or both exact matches in the past. Additionally, it also allows matching against the fragment and matrix parameters. fixes #13205 BREAKING CHANGE: The type of the `RouterLinkActive.routerLinkActiveOptions` input was expanded to allow more fine-tuned control. Code that previously read this property may need to be updated to account for the new type. PR Close #40303 --- goldens/public-api/router/router.d.ts | 12 +- .../size-tracking/integration-payloads.json | 4 +- .../router/bundle.golden_symbols.json | 15 ++ .../src/directives/router_link_active.ts | 20 ++- packages/router/src/index.ts | 2 +- packages/router/src/router.ts | 58 ++++++- packages/router/src/url_tree.ts | 118 +++++++++++-- packages/router/test/url_tree.spec.ts | 155 ++++++++++++++---- 8 files changed, 325 insertions(+), 59 deletions(-) diff --git a/goldens/public-api/router/router.d.ts b/goldens/public-api/router/router.d.ts index f0ac9d190f..b1aada813d 100644 --- a/goldens/public-api/router/router.d.ts +++ b/goldens/public-api/router/router.d.ts @@ -158,6 +158,13 @@ export declare class GuardsCheckStart extends RouterEvent { export declare type InitialNavigation = 'disabled' | 'enabled' | 'enabledBlocking' | 'enabledNonBlocking'; +export declare interface IsActiveMatchOptions { + fragment: 'exact' | 'ignored'; + matrixParams: 'exact' | 'subset' | 'ignored'; + paths: 'exact' | 'subset'; + queryParams: 'exact' | 'subset' | 'ignored'; +} + export declare type LoadChildren = LoadChildrenCallback | DeprecatedLoadChildren; export declare type LoadChildrenCallback = () => Type | NgModuleFactory | Observable> | Promise | Type | any>; @@ -345,7 +352,8 @@ export declare class Router { dispose(): void; getCurrentNavigation(): Navigation | null; initialNavigation(): void; - isActive(url: string | UrlTree, exact: boolean): boolean; + /** @deprecated */ isActive(url: string | UrlTree, exact: boolean): boolean; + isActive(url: string | UrlTree, matchOptions: IsActiveMatchOptions): boolean; navigate(commands: any[], extras?: NavigationExtras): Promise; navigateByUrl(url: string | UrlTree, extras?: NavigationBehaviorOptions): Promise; ngOnDestroy(): void; @@ -400,7 +408,7 @@ export declare class RouterLinkActive implements OnChanges, OnDestroy, AfterCont set routerLinkActive(data: string[] | string); routerLinkActiveOptions: { exact: boolean; - }; + } | IsActiveMatchOptions; constructor(router: Router, element: ElementRef, renderer: Renderer2, cdr: ChangeDetectorRef, link?: RouterLink | undefined, linkWithHref?: RouterLinkWithHref | undefined); ngAfterContentInit(): void; ngOnChanges(changes: SimpleChanges): void; diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json index aba3add9a9..a7fbd11db8 100644 --- a/goldens/size-tracking/integration-payloads.json +++ b/goldens/size-tracking/integration-payloads.json @@ -39,7 +39,7 @@ "master": { "uncompressed": { "runtime-es2015": 2285, - "main-es2015": 241843, + "main-es2015": 242531, "polyfills-es2015": 36709, "5-es2015": 745 } @@ -49,7 +49,7 @@ "master": { "uncompressed": { "runtime-es2015": 2289, - "main-es2015": 217591, + "main-es2015": 218317, "polyfills-es2015": 36723, "5-es2015": 781 } diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index d79400f0c5..9ea3c3a10f 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -1199,6 +1199,9 @@ { "name": "equalPath" }, + { + "name": "exactMatchOptions" + }, { "name": "executeCheckHooks" }, @@ -1661,6 +1664,9 @@ { "name": "materializeViewResults" }, + { + "name": "matrixParamsMatch" + }, { "name": "maybeUnwrapFn" }, @@ -1754,6 +1760,12 @@ { "name": "optionsReducer" }, + { + "name": "paramCompareMap" + }, + { + "name": "pathCompareMap" + }, { "name": "pipeFromArray" }, @@ -1949,6 +1961,9 @@ { "name": "subscribeToResult" }, + { + "name": "subsetMatchOptions" + }, { "name": "supportsState" }, diff --git a/packages/router/src/directives/router_link_active.ts b/packages/router/src/directives/router_link_active.ts index 080b6ec3ce..8617cb2e2c 100644 --- a/packages/router/src/directives/router_link_active.ts +++ b/packages/router/src/directives/router_link_active.ts @@ -12,6 +12,7 @@ import {mergeAll} from 'rxjs/operators'; import {Event, NavigationEnd} from '../events'; import {Router} from '../router'; +import {IsActiveMatchOptions} from '../url_tree'; import {RouterLink, RouterLinkWithHref} from './router_link'; @@ -89,7 +90,15 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit private linkInputChangesSubscription?: Subscription; public readonly isActive: boolean = false; - @Input() routerLinkActiveOptions: {exact: boolean} = {exact: false}; + /** + * Options to configure how to determine if the router link is active. + * + * These options are passed to the `Router.isActive()` function. + * + * @see Router.isActive + */ + @Input() routerLinkActiveOptions: {exact: boolean}|IsActiveMatchOptions = {exact: false}; + constructor( private router: Router, private element: ElementRef, private renderer: Renderer2, @@ -159,8 +168,11 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit } private isLinkActive(router: Router): (link: (RouterLink|RouterLinkWithHref)) => boolean { - return (link: RouterLink|RouterLinkWithHref) => - router.isActive(link.urlTree, this.routerLinkActiveOptions.exact); + const options = 'paths' in this.routerLinkActiveOptions ? + this.routerLinkActiveOptions : + // While the types should disallow `undefined` here, it's possible without strict inputs + (this.routerLinkActiveOptions.exact || false); + return (link: RouterLink|RouterLinkWithHref) => router.isActive(link.urlTree, options); } private hasActiveLinks(): boolean { @@ -169,4 +181,4 @@ export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit this.linkWithHref && isActiveCheckFn(this.linkWithHref) || this.links.some(isActiveCheckFn) || this.linksWithHrefs.some(isActiveCheckFn); } -} +} \ No newline at end of file diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index 688323fece..cc47eb1cf1 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -22,7 +22,7 @@ export {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} fr export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state'; export {convertToParamMap, ParamMap, Params, PRIMARY_OUTLET} from './shared'; export {UrlHandlingStrategy} from './url_handling_strategy'; -export {DefaultUrlSerializer, UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree'; +export {DefaultUrlSerializer, IsActiveMatchOptions, UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree'; export {VERSION} from './version'; export * from './private_export'; diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index cc41b81ae7..6661289646 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -27,7 +27,7 @@ import {ChildrenOutletContexts} from './router_outlet_context'; import {ActivatedRoute, createEmptyState, RouterState, RouterStateSnapshot} from './router_state'; import {isNavigationCancelingError, navigationCancelingError, Params} from './shared'; import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy'; -import {containsTree, createEmptyUrlTree, UrlSerializer, UrlTree} from './url_tree'; +import {containsTree, createEmptyUrlTree, IsActiveMatchOptions, UrlSerializer, UrlTree} from './url_tree'; import {standardizeConfig, validateConfig} from './utils/config'; import {Checks, getAllRouteGuards} from './utils/preactivation'; import {isUrlTree} from './utils/type_guards'; @@ -356,6 +356,29 @@ type LocationChangeInfo = { transitionId: number }; +/** + * The equivalent `IsActiveUrlTreeOptions` options for `Router.isActive` is called with `false` + * (exact = true). + */ +export const exactMatchOptions: IsActiveMatchOptions = { + paths: 'exact', + fragment: 'ignored', + matrixParams: 'ignored', + queryParams: 'exact' +}; + +/** + * The equivalent `IsActiveUrlTreeOptions` options for `Router.isActive` is called with `false` + * (exact = false). + */ +export const subsetMatchOptions: IsActiveMatchOptions = { + paths: 'subset', + fragment: 'ignored', + matrixParams: 'ignored', + queryParams: 'subset' +}; + + /** * @description * @@ -1213,14 +1236,39 @@ export class Router { return urlTree; } - /** Returns whether the url is activated */ - isActive(url: string|UrlTree, exact: boolean): boolean { + /** + * Returns whether the url is activated. + * + * @deprecated + * Use `IsActiveUrlTreeOptions` instead. + * + * - The equivalent `IsActiveUrlTreeOptions` for `true` is + * `{paths: 'exact', queryParams: 'exact', fragment: 'ignored', matrixParams: 'ignored'}`. + * - The equivalent for `false` is + * `{paths: 'subset', queryParams: 'subset', fragment: 'ignored', matrixParams: 'ignored'}`. + */ + isActive(url: string|UrlTree, exact: boolean): boolean; + /** + * Returns whether the url is activated. + */ + isActive(url: string|UrlTree, matchOptions: IsActiveMatchOptions): boolean; + /** @internal */ + isActive(url: string|UrlTree, matchOptions: boolean|IsActiveMatchOptions): boolean; + isActive(url: string|UrlTree, matchOptions: boolean|IsActiveMatchOptions): boolean { + let options: IsActiveMatchOptions; + if (matchOptions === true) { + options = {...exactMatchOptions}; + } else if (matchOptions === false) { + options = {...subsetMatchOptions}; + } else { + options = matchOptions; + } if (isUrlTree(url)) { - return containsTree(this.currentUrlTree, url, exact); + return containsTree(this.currentUrlTree, url, options); } const urlTree = this.parseUrl(url); - return containsTree(this.currentUrlTree, urlTree, exact); + return containsTree(this.currentUrlTree, urlTree, options); } private removeEmptyProps(params: Params): Params { diff --git a/packages/router/src/url_tree.ts b/packages/router/src/url_tree.ts index 8d943ab841..f1f8f1b742 100644 --- a/packages/router/src/url_tree.ts +++ b/packages/router/src/url_tree.ts @@ -13,53 +13,128 @@ export function createEmptyUrlTree() { return new UrlTree(new UrlSegmentGroup([], {}), {}, null); } -export function containsTree(container: UrlTree, containee: UrlTree, exact: boolean): boolean { - if (exact) { - return equalQueryParams(container.queryParams, containee.queryParams) && - equalSegmentGroups(container.root, containee.root); - } - - return containsQueryParams(container.queryParams, containee.queryParams) && - containsSegmentGroup(container.root, containee.root); +/** + * A set of options which specify how to determine if a `UrlTree` is active, given the `UrlTree` + * for the current router state. + * + * @publicApi + * @see Router.isActive + */ +export interface IsActiveMatchOptions { + /** + * Defines the strategy for comparing the matrix parameters of two `UrlTree`s. + * + * The matrix parameter matching is dependent on the strategy for matching the + * segments. That is, if the `paths` option is set to `'subset'`, only + * the matrix parameters of the matching segments will be compared. + * + * - `'exact'`: Requires that matching segments also have exact matrix parameter + * matches. + * - `'subset'`: The matching segments in the router's active `UrlTree` may contain + * 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'; + /** + * Defines the strategy for comparing the query parameters of two `UrlTree`s. + * + * - `'exact'`: the query parameters must match exactly. + * - `'subset'`: the active `UrlTree` may contain extra parameters, + * 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'; + /** + * Defines the strategy for comparing the `UrlSegment`s of the `UrlTree`s. + * + * - `'exact'`: all segments in each `UrlTree` must match. + * - `'subset'`: a `UrlTree` will be determined to be active if it + * is a subtree of the active route. That is, the active route may contain extra + * segments, but must at least have all the segements of the `UrlTree` in question. + */ + 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'; } -function equalQueryParams(container: Params, containee: Params): boolean { +type ParamMatchOptions = 'exact'|'subset'|'ignored'; + +type PathCompareFn = + (container: UrlSegmentGroup, containee: UrlSegmentGroup, matrixParams: ParamMatchOptions) => + boolean; +type ParamCompareFn = (container: Params, containee: Params) => boolean; + +const pathCompareMap: Record = { + 'exact': equalSegmentGroups, + 'subset': containsSegmentGroup, +}; +const paramCompareMap: Record = { + 'exact': equalParams, + 'subset': containsParams, + 'ignored': () => true, +}; + +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); +} + +function equalParams(container: Params, containee: Params): boolean { // TODO: This does not handle array params correctly. return shallowEqual(container, containee); } -function equalSegmentGroups(container: UrlSegmentGroup, containee: UrlSegmentGroup): boolean { +function equalSegmentGroups( + container: UrlSegmentGroup, containee: UrlSegmentGroup, + matrixParams: ParamMatchOptions): boolean { if (!equalPath(container.segments, containee.segments)) return false; + if (!matrixParamsMatch(container.segments, containee.segments, matrixParams)) { + return false; + } if (container.numberOfChildren !== containee.numberOfChildren) return false; for (const c in containee.children) { if (!container.children[c]) return false; - if (!equalSegmentGroups(container.children[c], containee.children[c])) return false; + if (!equalSegmentGroups(container.children[c], containee.children[c], matrixParams)) + return false; } return true; } -function containsQueryParams(container: Params, containee: Params): boolean { +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])); } -function containsSegmentGroup(container: UrlSegmentGroup, containee: UrlSegmentGroup): boolean { - return containsSegmentGroupHelper(container, containee, containee.segments); +function containsSegmentGroup( + container: UrlSegmentGroup, containee: UrlSegmentGroup, + matrixParams: ParamMatchOptions): boolean { + return containsSegmentGroupHelper(container, containee, containee.segments, matrixParams); } function containsSegmentGroupHelper( - container: UrlSegmentGroup, containee: UrlSegmentGroup, containeePaths: UrlSegment[]): 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; for (const c in containee.children) { if (!container.children[c]) return false; - if (!containsSegmentGroup(container.children[c], containee.children[c])) return false; + if (!containsSegmentGroup(container.children[c], containee.children[c], matrixParams)) { + return false; + } } return true; @@ -67,11 +142,20 @@ function containsSegmentGroupHelper( const current = containeePaths.slice(0, container.segments.length); const next = containeePaths.slice(container.segments.length); if (!equalPath(container.segments, current)) return false; + if (!matrixParamsMatch(container.segments, current, matrixParams)) return false; if (!container.children[PRIMARY_OUTLET]) return false; - return containsSegmentGroupHelper(container.children[PRIMARY_OUTLET], containee, next); + return containsSegmentGroupHelper( + container.children[PRIMARY_OUTLET], containee, next, matrixParams); } } +function matrixParamsMatch( + containerPaths: UrlSegment[], containeePaths: UrlSegment[], options: ParamMatchOptions) { + return containeePaths.every((containeeSegment, i) => { + return paramCompareMap[options](containerPaths[i].parameters, containeeSegment.parameters); + }); +} + /** * @description * diff --git a/packages/router/test/url_tree.spec.ts b/packages/router/test/url_tree.spec.ts index 34aac7b3b2..8adc90c2d7 100644 --- a/packages/router/test/url_tree.spec.ts +++ b/packages/router/test/url_tree.spec.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {exactMatchOptions, subsetMatchOptions} from '../src/router'; import {containsTree, DefaultUrlSerializer} from '../src/url_tree'; describe('UrlTree', () => { @@ -33,80 +34,80 @@ describe('UrlTree', () => { const url = '/one/(one//left:three)(right:four)'; const t1 = serializer.parse(url); const t2 = serializer.parse(url); - expect(containsTree(t1, t2, true)).toBe(true); - expect(containsTree(t2, t1, true)).toBe(true); + expect(containsTree(t1, t2, exactMatchOptions)).toBe(true); + expect(containsTree(t2, t1, exactMatchOptions)).toBe(true); }); it('should return true when queryParams are the same', () => { const t1 = serializer.parse('/one/two?test=1&page=5'); const t2 = serializer.parse('/one/two?test=1&page=5'); - expect(containsTree(t1, t2, true)).toBe(true); + expect(containsTree(t1, t2, exactMatchOptions)).toBe(true); }); it('should return true when queryParams are the same but with diffrent order', () => { const t1 = serializer.parse('/one/two?test=1&page=5'); const t2 = serializer.parse('/one/two?page=5&test=1'); - expect(containsTree(t1, t2, true)).toBe(true); + expect(containsTree(t1, t2, exactMatchOptions)).toBe(true); }); it('should return true when queryParams contains array params that are the same', () => { const t1 = serializer.parse('/one/two?test=a&test=b&pages=5&pages=6'); const t2 = serializer.parse('/one/two?test=a&test=b&pages=5&pages=6'); - expect(containsTree(t1, t2, true)).toBe(true); + expect(containsTree(t1, t2, exactMatchOptions)).toBe(true); }); it('should return false when queryParams contains array params but are not the same', () => { const t1 = serializer.parse('/one/two?test=a&test=b&pages=5&pages=6'); const t2 = serializer.parse('/one/two?test=a&test=b&pages=5&pages=7'); - expect(containsTree(t1, t2, false)).toBe(false); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(false); }); it('should return false when queryParams are not the same', () => { const t1 = serializer.parse('/one/two?test=1&page=5'); const t2 = serializer.parse('/one/two?test=1'); - expect(containsTree(t1, t2, true)).toBe(false); + expect(containsTree(t1, t2, exactMatchOptions)).toBe(false); }); it('should return false when queryParams are not the same', () => { const t1 = serializer.parse('/one/two?test=4&test=4&test=2'); const t2 = serializer.parse('/one/two?test=4&test=3&test=2'); - expect(containsTree(t1, t2, false)).toBe(false); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(false); }); it('should return true when queryParams are the same in different order', () => { const t1 = serializer.parse('/one/two?test=4&test=3&test=2'); const t2 = serializer.parse('/one/two?test=2&test=3&test=4'); - expect(containsTree(t1, t2, false)).toBe(true); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(true); }); it('should return true when queryParams are the same in different order', () => { const t1 = serializer.parse('/one/two?test=4&test=4&test=1'); const t2 = serializer.parse('/one/two?test=1&test=4&test=4'); - expect(containsTree(t1, t2, false)).toBe(true); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(true); }); it('should return false when containee is missing queryParams', () => { const t1 = serializer.parse('/one/two?page=5'); const t2 = serializer.parse('/one/two'); - expect(containsTree(t1, t2, true)).toBe(false); + expect(containsTree(t1, t2, exactMatchOptions)).toBe(false); }); it('should return false when paths are not the same', () => { const t1 = serializer.parse('/one/two(right:three)'); const t2 = serializer.parse('/one/two2(right:three)'); - expect(containsTree(t1, t2, true)).toBe(false); + expect(containsTree(t1, t2, exactMatchOptions)).toBe(false); }); it('should return false when container has an extra child', () => { const t1 = serializer.parse('/one/two(right:three)'); const t2 = serializer.parse('/one/two'); - expect(containsTree(t1, t2, true)).toBe(false); + expect(containsTree(t1, t2, exactMatchOptions)).toBe(false); }); it('should return false when containee has an extra child', () => { const t1 = serializer.parse('/one/two'); const t2 = serializer.parse('/one/two(right:three)'); - expect(containsTree(t1, t2, true)).toBe(false); + expect(containsTree(t1, t2, exactMatchOptions)).toBe(false); }); }); @@ -114,85 +115,183 @@ describe('UrlTree', () => { it('should return true when containee is missing a segment', () => { const t1 = serializer.parse('/one/(two//left:three)(right:four)'); const t2 = serializer.parse('/one/(two//left:three)'); - expect(containsTree(t1, t2, false)).toBe(true); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(true); }); it('should return true when containee is missing some paths', () => { const t1 = serializer.parse('/one/two/three'); const t2 = serializer.parse('/one/two'); - expect(containsTree(t1, t2, false)).toBe(true); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(true); }); it('should return true container has its paths split into multiple segments', () => { const t1 = serializer.parse('/one/(two//left:three)'); const t2 = serializer.parse('/one/two'); - expect(containsTree(t1, t2, false)).toBe(true); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(true); }); it('should return false when containee has extra segments', () => { const t1 = serializer.parse('/one/two'); const t2 = serializer.parse('/one/(two//left:three)'); - expect(containsTree(t1, t2, false)).toBe(false); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(false); }); it('should return false containee has segments that the container does not have', () => { const t1 = serializer.parse('/one/(two//left:three)'); const t2 = serializer.parse('/one/(two//right:four)'); - expect(containsTree(t1, t2, false)).toBe(false); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(false); }); it('should return false when containee has extra paths', () => { const t1 = serializer.parse('/one'); const t2 = serializer.parse('/one/two'); - expect(containsTree(t1, t2, false)).toBe(false); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(false); }); it('should return true when queryParams are the same', () => { const t1 = serializer.parse('/one/two?test=1&page=5'); const t2 = serializer.parse('/one/two?test=1&page=5'); - expect(containsTree(t1, t2, false)).toBe(true); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(true); }); it('should return true when container contains containees queryParams', () => { const t1 = serializer.parse('/one/two?test=1&u=5'); const t2 = serializer.parse('/one/two?u=5'); - expect(containsTree(t1, t2, false)).toBe(true); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(true); }); it('should return true when containee does not have queryParams', () => { const t1 = serializer.parse('/one/two?page=5'); const t2 = serializer.parse('/one/two'); - expect(containsTree(t1, t2, false)).toBe(true); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(true); }); it('should return false when containee has but container does not have queryParams', () => { const t1 = serializer.parse('/one/two'); const t2 = serializer.parse('/one/two?page=1'); - expect(containsTree(t1, t2, false)).toBe(false); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(false); }); it('should return true when container has array params but containee does not have', () => { const t1 = serializer.parse('/one/two?test=a&test=b&pages=5&pages=6'); const t2 = serializer.parse('/one/two?test=a&test=b'); - expect(containsTree(t1, t2, false)).toBe(true); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(true); }); it('should return false when containee has array params but container does not have', () => { const t1 = serializer.parse('/one/two?test=a&test=b'); const t2 = serializer.parse('/one/two?test=a&test=b&pages=5&pages=6'); - expect(containsTree(t1, t2, false)).toBe(false); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(false); }); it('should return false when containee has different queryParams', () => { const t1 = serializer.parse('/one/two?page=5'); const t2 = serializer.parse('/one/two?test=1'); - expect(containsTree(t1, t2, false)).toBe(false); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(false); }); it('should return false when containee has more queryParams than container', () => { const t1 = serializer.parse('/one/two?page=5'); const t2 = serializer.parse('/one/two?page=5&test=1'); - expect(containsTree(t1, t2, false)).toBe(false); + expect(containsTree(t1, t2, subsetMatchOptions)).toBe(false); + }); + }); + + describe('ignored query params', () => { + it('should return true when queryParams differ but are ignored', () => { + const t1 = serializer.parse('/?test=1&page=2'); + const t2 = serializer.parse('/?test=3&page=4&x=y'); + expect(containsTree(t1, t2, {...exactMatchOptions, queryParams: 'ignored'})).toBe(true); + }); + }); + + describe('fragment', () => { + it('should return false when fragments differ but options require exact match', () => { + const t1 = serializer.parse('/#fragment1'); + const t2 = serializer.parse('/#fragment2'); + expect(containsTree(t1, t2, {...exactMatchOptions, fragment: 'exact'})).toBe(false); + }); + + it('should return true when fragments differ but options ignore the fragment', () => { + const t1 = serializer.parse('/#fragment1'); + const t2 = serializer.parse('/#fragment2'); + expect(containsTree(t1, t2, {...exactMatchOptions, fragment: 'ignored'})).toBe(true); + }); + }); + + describe('matrix params', () => { + describe('ignored', () => { + it('returns true when matrix params differ but are ignored', () => { + const t1 = serializer.parse('/a;id=15;foo=foo'); + const t2 = serializer.parse('/a;abc=123'); + expect(containsTree(t1, t2, {...exactMatchOptions, matrixParams: 'ignored'})).toBe(true); + }); + }); + + describe('exact match', () => { + const matrixParams = 'exact'; + + it('returns true when matrix params match', () => { + const t1 = serializer.parse('/a;id=15;foo=foo'); + const t2 = serializer.parse('/a;id=15;foo=foo'); + expect(containsTree(t1, t2, {...exactMatchOptions, matrixParams})).toBe(true); + }); + + it('returns false when matrix params differ', () => { + const t1 = serializer.parse('/a;id=15;foo=foo'); + const t2 = serializer.parse('/a;abc=123'); + expect(containsTree(t1, t2, {...exactMatchOptions, matrixParams})).toBe(false); + }); + + it('returns true when matrix params match on the subset of the matched url tree', () => { + const t1 = serializer.parse('/a;id=15;foo=bar/c'); + const t2 = serializer.parse('/a;id=15;foo=bar'); + 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', () => { + const matrixParams = 'subset'; + + it('returns true when matrix params match', () => { + const t1 = serializer.parse('/a;id=15;foo=foo'); + const t2 = serializer.parse('/a;id=15;foo=foo'); + expect(containsTree(t1, t2, {...exactMatchOptions, matrixParams})).toBe(true); + }); + + it('returns true when container has extra matrix params', () => { + const t1 = serializer.parse('/a;id=15;foo=foo'); + const t2 = serializer.parse('/a;id=15'); + expect(containsTree(t1, t2, {...exactMatchOptions, matrixParams})).toBe(true); + }); + + it('returns false when matrix params differ', () => { + const t1 = serializer.parse('/a;id=15;foo=foo'); + const t2 = serializer.parse('/a;abc=123'); + expect(containsTree(t1, t2, {...exactMatchOptions, matrixParams})).toBe(false); + }); + + it('returns true when matrix params match on the subset of the matched url tree', () => { + const t1 = serializer.parse('/a;id=15;foo=bar/c'); + const t2 = serializer.parse('/a;id=15;foo=bar'); + 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); + }); }); }); });