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
This commit is contained in:
Andrew Scott 2020-12-29 14:41:42 -08:00 committed by Zach Arend
parent 29d8a0ab09
commit 6c05c80f19
8 changed files with 325 additions and 59 deletions

View File

@ -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<any> | NgModuleFactory<any> | Observable<Type<any>> | Promise<NgModuleFactory<any> | Type<any> | 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<boolean>;
navigateByUrl(url: string | UrlTree, extras?: NavigationBehaviorOptions): Promise<boolean>;
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;

View File

@ -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
}

View File

@ -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"
},

View File

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

View File

@ -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';

View File

@ -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 {

View File

@ -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<IsActiveMatchOptions['paths'], PathCompareFn> = {
'exact': equalSegmentGroups,
'subset': containsSegmentGroup,
};
const paramCompareMap: Record<ParamMatchOptions, ParamCompareFn> = {
'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
*

View File

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