mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
841 lines
31 KiB
TypeScript
841 lines
31 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
|
|
import {Component, Injectable} from '@angular/core';
|
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
|
import {By} from '@angular/platform-browser';
|
|
|
|
import {createUrlTreeFromSnapshot} from '../src/create_url_tree';
|
|
import {QueryParamsHandling, Routes} from '../src/models';
|
|
import {Router} from '../src/router';
|
|
import {RouterModule} from '../src/router_module';
|
|
import {ActivatedRoute, ActivatedRouteSnapshot} from '../src/router_state';
|
|
import {Params, PRIMARY_OUTLET} from '../src/shared';
|
|
import {DefaultUrlSerializer, UrlSerializer, UrlTree} from '../src/url_tree';
|
|
import {provideRouter, withRouterConfig} from '../src';
|
|
import {timeout} from '@angular/private/testing';
|
|
|
|
describe('createUrlTree', () => {
|
|
const serializer = new DefaultUrlSerializer();
|
|
let router: Router;
|
|
beforeEach(() => {
|
|
router = TestBed.inject(Router);
|
|
router.resetConfig([
|
|
{
|
|
path: 'parent',
|
|
children: [
|
|
{path: 'child', component: class {}},
|
|
{path: '**', outlet: 'secondary', component: class {}},
|
|
],
|
|
},
|
|
{
|
|
path: 'a',
|
|
children: [
|
|
{path: '**', component: class {}},
|
|
{path: '**', outlet: 'right', component: class {}},
|
|
{path: '**', outlet: 'left', component: class {}},
|
|
],
|
|
},
|
|
{path: '**', component: class {}},
|
|
{path: '**', outlet: 'right', component: class {}},
|
|
{path: '**', outlet: 'left', component: class {}},
|
|
{path: '**', outlet: 'rootSecondary', component: class {}},
|
|
]);
|
|
});
|
|
|
|
describe('query parameters', () => {
|
|
it('should support parameter with multiple values', async () => {
|
|
const p1 = serializer.parse('/');
|
|
const t1 = await createRoot(p1, ['/'], {m: ['v1', 'v2']});
|
|
expect(serializer.serialize(t1)).toEqual('/?m=v1&m=v2');
|
|
|
|
await router.navigateByUrl('/a/c');
|
|
const t2 = create(router.routerState.root.children[0].children[0], ['c2'], {m: ['v1', 'v2']});
|
|
expect(serializer.serialize(t2)).toEqual('/a/c/c2?m=v1&m=v2');
|
|
});
|
|
|
|
it('should support parameter with empty arrays as values', async () => {
|
|
await router.navigateByUrl('/a/c');
|
|
const t1 = create(router.routerState.root.children[0].children[0], ['c2'], {m: []});
|
|
expect(serializer.serialize(t1)).toEqual('/a/c/c2');
|
|
|
|
const t2 = create(router.routerState.root.children[0].children[0], ['c2'], {m: [], n: 1});
|
|
expect(serializer.serialize(t2)).toEqual('/a/c/c2?n=1');
|
|
});
|
|
|
|
it('should support parameter with single-item array', async () => {
|
|
await router.navigateByUrl('/a/c');
|
|
const t1 = create(router.routerState.root.children[0].children[0], ['c2'], {m: [1]});
|
|
expect(serializer.serialize(t1)).toEqual('/a/c/c2?m=1');
|
|
expect(t1.queryParams['m']).toEqual(['1']);
|
|
});
|
|
|
|
it('should set query params', async () => {
|
|
const p = serializer.parse('/');
|
|
const t = await createRoot(p, [], {a: 'hey'});
|
|
expect(t.queryParams).toEqual({a: 'hey'});
|
|
expect(t.queryParamMap.get('a')).toEqual('hey');
|
|
});
|
|
|
|
it('should stringify query params', async () => {
|
|
const p = serializer.parse('/');
|
|
const t = await createRoot(p, [], {a: 1});
|
|
expect(t.queryParams).toEqual({a: '1'});
|
|
expect(t.queryParamMap.get('a')).toEqual('1');
|
|
});
|
|
|
|
it('should retain query parameter order', async () => {
|
|
await router.navigateByUrl('/a/c');
|
|
const t = create(router.routerState.root.children[0].children[0], ['c2'], {
|
|
z: 1,
|
|
a: '2',
|
|
m: [3, 4],
|
|
b: '5',
|
|
});
|
|
expect(serializer.serialize(t)).toEqual('/a/c/c2?z=1&a=2&m=3&m=4&b=5');
|
|
});
|
|
});
|
|
|
|
it('should navigate to the root', async () => {
|
|
const p = serializer.parse('/');
|
|
const t = await createRoot(p, ['/']);
|
|
expect(serializer.serialize(t)).toEqual('/');
|
|
});
|
|
|
|
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/,
|
|
);
|
|
});
|
|
|
|
it('should support nested segments', async () => {
|
|
const p = serializer.parse('/a/b');
|
|
const t = await createRoot(p, ['/one', 11, 'two', 22]);
|
|
expect(serializer.serialize(t)).toEqual('/one/11/two/22');
|
|
});
|
|
|
|
it('should stringify positional parameters', async () => {
|
|
const p = serializer.parse('/a/b');
|
|
const t = await createRoot(p, ['/one', 11]);
|
|
const params = t.root.children[PRIMARY_OUTLET].segments;
|
|
expect(params[0].path).toEqual('one');
|
|
expect(params[1].path).toEqual('11');
|
|
});
|
|
|
|
it('should support first segments containing slashes', async () => {
|
|
const p = serializer.parse('/');
|
|
const t = await createRoot(p, [{segmentPath: '/one'}, 'two/three']);
|
|
expect(serializer.serialize(t)).toEqual('/%2Fone/two%2Fthree');
|
|
});
|
|
|
|
describe('named outlets', () => {
|
|
it('should preserve secondary segments', async () => {
|
|
const p = serializer.parse('/a/11/b(right:c)');
|
|
const t = await createRoot(p, ['/a', 11, 'd']);
|
|
expect(serializer.serialize(t)).toEqual('/a/11/d(right:c)');
|
|
});
|
|
|
|
it('should support updating secondary segments (absolute)', async () => {
|
|
const p = serializer.parse('/a(right:b)');
|
|
const t = await createRoot(p, ['/', {outlets: {right: ['c']}}]);
|
|
expect(serializer.serialize(t)).toEqual('/a(right:c)');
|
|
});
|
|
|
|
it('should support updating secondary segments', async () => {
|
|
const p = serializer.parse('/a(right:b)');
|
|
const t = await createRoot(p, [{outlets: {right: ['c', 11, 'd']}}]);
|
|
expect(serializer.serialize(t)).toEqual('/a(right:c/11/d)');
|
|
});
|
|
|
|
it('should support updating secondary segments (nested case)', async () => {
|
|
const p = serializer.parse('/a/(b//right:c)');
|
|
const t = await createRoot(p, ['a', {outlets: {right: ['d', 11, 'e']}}]);
|
|
expect(serializer.serialize(t)).toEqual('/a/(b//right:d/11/e)');
|
|
});
|
|
it('should support removing secondary outlet with prefix', async () => {
|
|
const p = serializer.parse('/parent/(child//secondary:popup)');
|
|
const t = await createRoot(p, ['parent', {outlets: {secondary: null}}]);
|
|
// - Segment index 0:
|
|
// * match and keep existing 'parent'
|
|
// - Segment index 1:
|
|
// * 'secondary' outlet cleared with `null`
|
|
// * 'primary' outlet not provided in the commands list, so the existing value is kept
|
|
expect(serializer.serialize(t)).toEqual('/parent/child');
|
|
});
|
|
|
|
it('should support updating secondary and primary outlets with prefix', async () => {
|
|
const p = serializer.parse('/parent/child');
|
|
const t = await createRoot(p, ['parent', {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('');
|
|
const t = await createRoot(p, ['parent', {outlets: {primary: 'child', secondary: 'popup'}}]);
|
|
expect(serializer.serialize(t)).toEqual('/parent/(child//secondary:popup)');
|
|
});
|
|
|
|
it('should support updating clearing primary and secondary with prefix', async () => {
|
|
const p = serializer.parse('/parent/(child//secondary:popup)');
|
|
const t = await createRoot(p, ['other']);
|
|
// Because we navigate away from the 'parent' route, the children of that route are cleared
|
|
// because they are note valid for the 'other' path.
|
|
expect(serializer.serialize(t)).toEqual('/other');
|
|
});
|
|
|
|
it('should not clear secondary outlet when at root and prefix is used', async () => {
|
|
const p = serializer.parse('/other(rootSecondary:rootPopup)');
|
|
const t = await createRoot(p, ['parent', {outlets: {primary: 'child', rootSecondary: null}}]);
|
|
// We prefixed the navigation with 'parent' so we cannot clear the "rootSecondary" outlet
|
|
// because once the outlets object is consumed, traversal is beyond the root segment.
|
|
expect(serializer.serialize(t)).toEqual('/parent/child(rootSecondary:rootPopup)');
|
|
});
|
|
|
|
it('should not clear non-root secondary outlet when command is targeting root', async () => {
|
|
const p = serializer.parse('/parent/(child//secondary:popup)');
|
|
const t = await createRoot(p, [{outlets: {secondary: null}}]);
|
|
// The start segment index for the command is at 0, but the outlet lives at index 1
|
|
// so we cannot clear the outlet from processing segment index 0.
|
|
expect(serializer.serialize(t)).toEqual('/parent/(child//secondary:popup)');
|
|
});
|
|
|
|
it('can clear an auxiliary outlet at the correct segment level', async () => {
|
|
const p = serializer.parse('/parent/(child//secondary:popup)(rootSecondary:rootPopup)');
|
|
// ^^^^^^^^^^^^^^^^^^^^^^
|
|
// The parens here show that 'child' and 'secondary:popup' appear at the same 'level' in the
|
|
// config, i.e. are part of the same children list. You can also imagine an implicit paren
|
|
// group around the whole URL to visualize how 'parent' and 'rootSecondary:rootPopup' are also
|
|
// defined at the same level.
|
|
const t = await createRoot(p, ['parent', {outlets: {primary: 'child', secondary: null}}]);
|
|
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('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'},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
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');
|
|
});
|
|
|
|
describe('absolute navigations', () => {
|
|
it('with and pathless root', async () => {
|
|
router.resetConfig([
|
|
{
|
|
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)');
|
|
});
|
|
it('empty path parent and sibling with a path', async () => {
|
|
router.resetConfig([
|
|
{
|
|
path: '',
|
|
children: [
|
|
{path: 'x', component: class {}},
|
|
{path: '**', outlet: 'left', component: class {}},
|
|
],
|
|
},
|
|
]);
|
|
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)');
|
|
});
|
|
|
|
it('empty path parent and sibling', async () => {
|
|
router.resetConfig([
|
|
{
|
|
path: '',
|
|
children: [
|
|
{path: '', component: class {}},
|
|
{path: '**', outlet: 'left', component: class {}},
|
|
{path: '**', outlet: 'right', component: class {}},
|
|
],
|
|
},
|
|
]);
|
|
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)');
|
|
});
|
|
it('two pathless parents', async () => {
|
|
router.resetConfig([
|
|
{
|
|
path: '',
|
|
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)');
|
|
});
|
|
|
|
it('maintains structure when primary outlet is not pathless', async () => {
|
|
router.resetConfig([
|
|
{
|
|
path: 'a',
|
|
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)');
|
|
});
|
|
});
|
|
});
|
|
|
|
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'}}]}},
|
|
]);
|
|
expect(serializer.serialize(t)).toEqual('/child/nested-primary');
|
|
});
|
|
|
|
it('should throw when outlets is not the last command', async () => {
|
|
const p = serializer.parse('/a');
|
|
await expectAsync(createRoot(p, ['a', {outlets: {right: ['c']}}, 'c'])).toBeRejected();
|
|
});
|
|
|
|
it('should support updating using a string', async () => {
|
|
const p = serializer.parse('/a(right:b)');
|
|
const t = await createRoot(p, [{outlets: {right: 'c/11/d'}}]);
|
|
expect(serializer.serialize(t)).toEqual('/a(right:c/11/d)');
|
|
});
|
|
|
|
it('should support updating primary and secondary segments at once', async () => {
|
|
const p = serializer.parse('/a(right:b)');
|
|
const t = await createRoot(p, [{outlets: {primary: 'y/z', right: 'c/11/d'}}]);
|
|
expect(serializer.serialize(t)).toEqual('/y/z(right:c/11/d)');
|
|
});
|
|
|
|
it('should support removing primary segment', async () => {
|
|
const p = serializer.parse('/a/(b//right:c)');
|
|
const t = await createRoot(p, ['a', {outlets: {primary: null, right: 'd'}}]);
|
|
expect(serializer.serialize(t)).toEqual('/a/(right:d)');
|
|
});
|
|
|
|
it('should support removing secondary segments', async () => {
|
|
const p = serializer.parse('/a(right:b)');
|
|
const t = await createRoot(p, [{outlets: {right: null}}]);
|
|
expect(serializer.serialize(t)).toEqual('/a');
|
|
});
|
|
|
|
it('should support removing parenthesis for primary segment on second path element', async () => {
|
|
const p = serializer.parse('/a/(b//right:c)');
|
|
const t = await createRoot(p, ['a', {outlets: {right: null}}]);
|
|
expect(serializer.serialize(t)).toEqual('/a/b');
|
|
});
|
|
|
|
it('should update matrix parameters', async () => {
|
|
const p = serializer.parse('/a;pp=11');
|
|
const t = await createRoot(p, ['/a', {pp: 22, dd: 33}]);
|
|
expect(serializer.serialize(t)).toEqual('/a;pp=22;dd=33');
|
|
});
|
|
|
|
it('should create matrix parameters', async () => {
|
|
const p = serializer.parse('/a');
|
|
const t = await createRoot(p, ['/a', {pp: 22, dd: 33}]);
|
|
expect(serializer.serialize(t)).toEqual('/a;pp=22;dd=33');
|
|
});
|
|
|
|
it('should create matrix parameters together with other segments', async () => {
|
|
const p = serializer.parse('/a');
|
|
const t = await createRoot(p, ['/a', 'b', {aa: 22, bb: 33}]);
|
|
expect(serializer.serialize(t)).toEqual('/a/b;aa=22;bb=33');
|
|
});
|
|
|
|
it('should stringify matrix parameters', async () => {
|
|
await router.navigateByUrl('/a');
|
|
const relative = create(router.routerState.root.children[0], [{pp: 22}]);
|
|
const segmentR = relative.root.children[PRIMARY_OUTLET].segments[0];
|
|
expect(segmentR.parameterMap.get('pp')).toEqual('22');
|
|
|
|
const pa = serializer.parse('/a');
|
|
const absolute = await createRoot(pa, ['/b', {pp: 33}]);
|
|
const segmentA = absolute.root.children[PRIMARY_OUTLET].segments[0];
|
|
expect(segmentA.parameterMap.get('pp')).toEqual('33');
|
|
});
|
|
|
|
describe('relative navigation', () => {
|
|
it('should work', async () => {
|
|
await router.navigateByUrl('/a/(c//left:cp)(left:ap)');
|
|
const t = create(router.routerState.root.children[0], ['c2']);
|
|
expect(serializer.serialize(t)).toEqual('/a/(c2//left:cp)(left:ap)');
|
|
});
|
|
|
|
it('should work when the first command starts with a ./', async () => {
|
|
await router.navigateByUrl('/a/(c//left:cp)(left:ap)');
|
|
const t = create(router.routerState.root.children[0], ['./c2']);
|
|
expect(serializer.serialize(t)).toEqual('/a/(c2//left:cp)(left:ap)');
|
|
});
|
|
|
|
it('should work when the first command is ./)', async () => {
|
|
await router.navigateByUrl('/a/(c//left:cp)(left:ap)');
|
|
const t = create(router.routerState.root.children[0], ['./', 'c2']);
|
|
expect(serializer.serialize(t)).toEqual('/a/(c2//left:cp)(left:ap)');
|
|
});
|
|
|
|
it('should support parameters-only navigation', async () => {
|
|
await router.navigateByUrl('/a');
|
|
const t = create(router.routerState.root.children[0], [{k: 99}]);
|
|
expect(serializer.serialize(t)).toEqual('/a;k=99');
|
|
});
|
|
|
|
it('should support parameters-only navigation (nested case)', async () => {
|
|
await router.navigateByUrl('/a/(c//left:cp)(left:ap)');
|
|
const t = create(router.routerState.root.children[0], [{'x': 99}]);
|
|
expect(serializer.serialize(t)).toEqual('/a;x=99(left:ap)');
|
|
});
|
|
|
|
it('should support parameters-only navigation (with a double dot)', async () => {
|
|
await router.navigateByUrl('/a/(c//left:cp)(left:ap)');
|
|
const t = create(router.routerState.root.children[0].children[0], ['../', {x: 5}]);
|
|
expect(serializer.serialize(t)).toEqual('/a;x=5(left:ap)');
|
|
});
|
|
|
|
it('should work when index > 0', async () => {
|
|
await router.navigateByUrl('/a/c');
|
|
const t = create(router.routerState.root.children[0].children[0], ['c2']);
|
|
expect(serializer.serialize(t)).toEqual('/a/c/c2');
|
|
});
|
|
|
|
it('should support going to a parent (within a segment)', async () => {
|
|
await router.navigateByUrl('/a/c');
|
|
const t = create(router.routerState.root.children[0].children[0], ['../c2']);
|
|
expect(serializer.serialize(t)).toEqual('/a/c2');
|
|
});
|
|
|
|
it('should support going to a parent (across segments)', async () => {
|
|
await router.navigateByUrl('/q/(a/(c//left:cp)//left:qp)(left:ap)');
|
|
// The resulting URL isn't necessarily correct. Though we could never truly navigate to the
|
|
// URL above, this should probably go to '/q/a/c(left:ap)' at the very least and potentially
|
|
// be able to go somewhere like /q/a/c/(left:xyz)(left:ap). That is, allow matching named
|
|
// outlets as long as they do not have a primary outlet sibling. Having a primary outlet
|
|
// sibling isn't possible because the wildcard should consume all the primary outlet segments
|
|
// so there cannot be any remaining in the children.
|
|
// https://github.com/angular/angular/issues/40089
|
|
expect(router.url).toEqual('/q(left:ap)');
|
|
|
|
const t = create(router.routerState.root.children[0].children[0], ['../../q2']);
|
|
expect(serializer.serialize(t)).toEqual('/q2(left:ap)');
|
|
});
|
|
|
|
it('should navigate to the root', async () => {
|
|
await router.navigateByUrl('/a/c');
|
|
const t = create(router.routerState.root.children[0], ['../']);
|
|
expect(serializer.serialize(t)).toEqual('/');
|
|
});
|
|
|
|
it('should work with ../ when absolute url', async () => {
|
|
await router.navigateByUrl('/a/c');
|
|
const t = create(router.routerState.root.children[0].children[0], ['../', 'c2']);
|
|
expect(serializer.serialize(t)).toEqual('/a/c2');
|
|
});
|
|
|
|
it('should work relative to root', async () => {
|
|
await router.navigateByUrl('/');
|
|
const t = create(router.routerState.root, ['11']);
|
|
expect(serializer.serialize(t)).toEqual('/11');
|
|
});
|
|
|
|
it('should throw when too many ..', async () => {
|
|
await router.navigateByUrl('/a/(c//left:cp)(left:ap)');
|
|
expect(() => create(router.routerState.root.children[0], ['../../'])).toThrowError();
|
|
});
|
|
|
|
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']}},
|
|
]);
|
|
expect(serializer.serialize(t)).toEqual('/a/b/(right:c)');
|
|
});
|
|
});
|
|
|
|
it('should set fragment', async () => {
|
|
const p = serializer.parse('/');
|
|
const t = await createRoot(p, [], {}, 'fragment');
|
|
expect(t.fragment).toEqual('fragment');
|
|
});
|
|
|
|
it('should support pathless route', async () => {
|
|
const p = serializer.parse('/a');
|
|
const t = create(router.routerState.root.children[0], ['b']);
|
|
expect(serializer.serialize(t)).toEqual('/b');
|
|
});
|
|
|
|
it('should support pathless route with ../ at root', async () => {
|
|
const p = serializer.parse('/a');
|
|
const t = create(router.routerState.root.children[0], ['../b']);
|
|
expect(serializer.serialize(t)).toEqual('/b');
|
|
});
|
|
|
|
it('should support pathless child of pathless root', async () => {
|
|
router.resetConfig([
|
|
{
|
|
path: '',
|
|
children: [
|
|
{path: '', component: class {}},
|
|
{path: 'lazy', component: class {}},
|
|
],
|
|
},
|
|
]);
|
|
await router.navigateByUrl('/');
|
|
const t = create(router.routerState.root.children[0].children[0], ['lazy']);
|
|
expect(serializer.serialize(t)).toEqual('/lazy');
|
|
});
|
|
});
|
|
|
|
describe('defaultQueryParamsHandling', () => {
|
|
async function setupRouter(defaultQueryParamsHandling: QueryParamsHandling): Promise<Router> {
|
|
TestBed.configureTestingModule({
|
|
providers: [
|
|
provideRouter(
|
|
[{path: '**', component: class {}}],
|
|
withRouterConfig({
|
|
defaultQueryParamsHandling,
|
|
}),
|
|
),
|
|
],
|
|
});
|
|
|
|
const router = TestBed.inject(Router);
|
|
await router.navigateByUrl('/initial?a=1');
|
|
return router;
|
|
}
|
|
|
|
it('can use "merge" as the default', async () => {
|
|
const router = await setupRouter('merge');
|
|
await router.navigate(['new'], {queryParams: {'b': 2}});
|
|
expect(router.url).toEqual('/new?a=1&b=2');
|
|
});
|
|
|
|
it('can use "perserve" as the default', async () => {
|
|
const router = await setupRouter('preserve');
|
|
await router.navigate(['new'], {queryParams: {'b': 2}});
|
|
expect(router.url).toEqual('/new?a=1');
|
|
});
|
|
|
|
it('can override the default by providing a new option', async () => {
|
|
const router = await setupRouter('preserve');
|
|
await router.navigate(['new'], {queryParams: {'b': 2}, queryParamsHandling: 'merge'});
|
|
expect(router.url).toEqual('/new?a=1&b=2');
|
|
await router.navigate(['replace'], {queryParamsHandling: 'replace'});
|
|
expect(router.url).toEqual('/replace');
|
|
});
|
|
});
|
|
|
|
async function createRoot(
|
|
tree: UrlTree,
|
|
commands: readonly any[],
|
|
queryParams?: Params,
|
|
fragment?: string,
|
|
): Promise<UrlTree> {
|
|
const router = TestBed.inject(Router);
|
|
await router.navigateByUrl(tree);
|
|
return router.createUrlTree(commands, {
|
|
relativeTo: router.routerState.root,
|
|
queryParams,
|
|
fragment,
|
|
});
|
|
}
|
|
|
|
function create(
|
|
relativeTo: ActivatedRoute,
|
|
commands: readonly any[],
|
|
queryParams?: Params,
|
|
fragment?: string,
|
|
) {
|
|
return TestBed.inject(Router).createUrlTree(commands, {relativeTo, queryParams, fragment});
|
|
}
|
|
|
|
describe('createUrlTreeFromSnapshot', () => {
|
|
it('can create a UrlTree relative to empty path named parent', async () => {
|
|
@Component({
|
|
template: `<router-outlet></router-outlet>`,
|
|
imports: [RouterModule],
|
|
})
|
|
class MainPageComponent {
|
|
constructor(
|
|
private route: ActivatedRoute,
|
|
private router: Router,
|
|
) {}
|
|
|
|
navigate() {
|
|
this.router.navigateByUrl(
|
|
createUrlTreeFromSnapshot(this.route.snapshot, ['innerRoute'], null, null),
|
|
);
|
|
}
|
|
}
|
|
|
|
@Component({
|
|
template: 'child works!',
|
|
standalone: false,
|
|
})
|
|
class ChildComponent {}
|
|
|
|
@Component({
|
|
template: '<router-outlet name="main-page"></router-outlet>',
|
|
imports: [RouterModule],
|
|
})
|
|
class RootCmp {}
|
|
|
|
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);
|
|
|
|
router.initialNavigation();
|
|
await advance(fixture);
|
|
fixture.debugElement.query(By.directive(MainPageComponent)).componentInstance.navigate();
|
|
await advance(fixture);
|
|
expect(fixture.nativeElement.innerHTML).toContain('child works!');
|
|
});
|
|
|
|
it('can navigate to relative to `ActivatedRouteSnapshot` in guard', async () => {
|
|
@Injectable({providedIn: 'root'})
|
|
class Guard {
|
|
constructor(private readonly router: Router) {}
|
|
canActivate(snapshot: ActivatedRouteSnapshot) {
|
|
this.router.navigateByUrl(createUrlTreeFromSnapshot(snapshot, ['../sibling'], null, null));
|
|
}
|
|
}
|
|
|
|
@Component({
|
|
template: `main`,
|
|
imports: [RouterModule],
|
|
})
|
|
class GuardedComponent {}
|
|
|
|
@Component({template: 'sibling'})
|
|
class SiblingComponent {}
|
|
|
|
@Component({
|
|
template: '<router-outlet></router-outlet>',
|
|
imports: [RouterModule],
|
|
})
|
|
class RootCmp {}
|
|
|
|
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);
|
|
|
|
router.navigateByUrl('parent/guarded');
|
|
await advance(fixture);
|
|
expect(router.url).toEqual('/parent/sibling');
|
|
});
|
|
});
|
|
|
|
async function advance(fixture: ComponentFixture<unknown>) {
|
|
await timeout();
|
|
fixture.detectChanges();
|
|
}
|
|
|
|
describe('createUrlTree with custom serializer', () => {
|
|
class CustomUrlSerializer extends DefaultUrlSerializer {
|
|
override parse(url: string): UrlTree {
|
|
const tree = super.parse(url);
|
|
const qp: {[key: string]: any} = {};
|
|
Object.keys(tree.queryParams).forEach((key) => {
|
|
const value = tree.queryParams[key];
|
|
if (typeof value === 'string' && (value.startsWith('{') || value.startsWith('['))) {
|
|
try {
|
|
qp[key] = JSON.parse(value);
|
|
} catch {}
|
|
} else {
|
|
qp[key] = value;
|
|
}
|
|
});
|
|
tree.queryParams = qp;
|
|
return tree;
|
|
}
|
|
override serialize(tree: UrlTree): string {
|
|
const qp: {[key: string]: any} = {};
|
|
Object.keys(tree.queryParams).forEach((key) => {
|
|
const value = tree.queryParams[key];
|
|
if (typeof value === 'object' && value !== null) {
|
|
qp[key] = JSON.stringify(value);
|
|
} else {
|
|
qp[key] = value;
|
|
}
|
|
});
|
|
tree.queryParams = qp;
|
|
return super.serialize(tree);
|
|
}
|
|
}
|
|
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({
|
|
providers: [provideRouter([]), {provide: UrlSerializer, useClass: CustomUrlSerializer}],
|
|
});
|
|
});
|
|
|
|
it('should work with custom URL serializer that handles objects', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const serializer = TestBed.inject(UrlSerializer);
|
|
const tree = router.createUrlTree(['a'], {queryParams: {someObject: {a: 1}}});
|
|
const url = serializer.serialize(tree);
|
|
expect(url).toEqual('/a?someObject=%7B%22a%22:1%7D');
|
|
const parsedTree = serializer.parse(url);
|
|
expect(parsedTree.queryParams).toEqual({someObject: {a: 1}});
|
|
});
|
|
|
|
it('should work with custom URL serializer and empty arrays', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const serializer = TestBed.inject(UrlSerializer);
|
|
const tree = router.createUrlTree(['a'], {queryParams: {empty: []}});
|
|
const url = serializer.serialize(tree);
|
|
// with default serializer, 'empty' would be removed because serialization maps arrays to multiple
|
|
// key-value pairs, and an empty array would result in no key-value pairs
|
|
expect(url).toEqual('/a?empty=%5B%5D');
|
|
const parsedTree = serializer.parse(url);
|
|
expect(parsedTree.queryParams).toEqual({empty: []});
|
|
});
|
|
|
|
it('should work with custom URL serializer and single item arrays', async () => {
|
|
const router = TestBed.inject(Router);
|
|
const serializer = TestBed.inject(UrlSerializer);
|
|
const tree = router.createUrlTree(['a'], {queryParams: {one: ['1']}});
|
|
const url = serializer.serialize(tree);
|
|
expect(url).toEqual('/a?one=%5B%221%22%5D');
|
|
const parsedTree = serializer.parse(url);
|
|
// with default serializer, 'one' would be removed because serialization maps arrays to multiple
|
|
// key-value pairs, and a single item array would result in one key-value pair, which would be
|
|
// deserialized to a string, not an array
|
|
expect(parsedTree.queryParams).toEqual({one: ['1']});
|
|
});
|
|
});
|