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); + }); }); }); });