mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
1e79dd3140
Add handling in navigation for creating and destroying injectors scoped to `ActivatedRoute` life. The code for creating the injectors is certainly more complicated than it _could_ be since there's no actual feature built around this yet. Keeps as much implementation code tree-shakeable as possible: Raw size: +764 bytes Gzipped size: +182 bytes
268 lines
8.7 KiB
TypeScript
268 lines
8.7 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 {EnvironmentInjector} from '@angular/core';
|
|
import {TestBed} from '@angular/core/testing';
|
|
import {map} from 'rxjs/operators';
|
|
|
|
import {createRouterState} from '../src/create_router_state';
|
|
import {Routes} from '../src/models';
|
|
import {recognize} from '../src/recognize';
|
|
import {DefaultRouteReuseStrategy} from '../src/route_reuse_strategy';
|
|
import {RouterConfigLoader} from '../src/router_config_loader';
|
|
import {
|
|
ActivatedRoute,
|
|
advanceActivatedRoute,
|
|
createEmptyState,
|
|
DEFAULT_PARAMS_INHERITANCE_STRATEGY,
|
|
RouterState,
|
|
RouterStateSnapshot,
|
|
} from '../src/router_state';
|
|
import {PRIMARY_OUTLET} from '../src/shared';
|
|
import {DefaultUrlSerializer, UrlTree} from '../src/url_tree';
|
|
import {TreeNode} from '../src/utils/tree';
|
|
|
|
describe('create router state', () => {
|
|
let reuseStrategy: DefaultRouteReuseStrategy;
|
|
beforeEach(() => {
|
|
reuseStrategy = new DefaultRouteReuseStrategy();
|
|
});
|
|
|
|
const emptyState = () => createEmptyState(RootComponent, TestBed.inject(EnvironmentInjector));
|
|
|
|
it('should create new state', async () => {
|
|
const {state, newlyCreatedRoutes} = createRouterState(
|
|
reuseStrategy,
|
|
await createState(
|
|
[
|
|
{path: 'a', component: ComponentA},
|
|
{path: 'b', component: ComponentB, outlet: 'left'},
|
|
{path: 'c', component: ComponentC, outlet: 'right'},
|
|
],
|
|
'a(left:b//right:c)',
|
|
),
|
|
emptyState(),
|
|
);
|
|
|
|
checkActivatedRoute(state.root, RootComponent);
|
|
|
|
const c = (state as any).children(state.root);
|
|
checkActivatedRoute(c[0], ComponentA);
|
|
checkActivatedRoute(c[1], ComponentB, 'left');
|
|
checkActivatedRoute(c[2], ComponentC, 'right');
|
|
|
|
expect(newlyCreatedRoutes.has(state.root)).toBe(false);
|
|
expect(newlyCreatedRoutes.has(c[0])).toBe(true);
|
|
expect(newlyCreatedRoutes.has(c[1])).toBe(true);
|
|
expect(newlyCreatedRoutes.has(c[2])).toBe(true);
|
|
expect(newlyCreatedRoutes.size).toBe(3);
|
|
});
|
|
|
|
it('should reuse existing nodes when it can', async () => {
|
|
const config = [
|
|
{path: 'a', component: ComponentA},
|
|
{path: 'b', component: ComponentB, outlet: 'left'},
|
|
{path: 'c', component: ComponentC, outlet: 'left'},
|
|
];
|
|
|
|
const {state: prevState, newlyCreatedRoutes: prevCreated} = createRouterState(
|
|
reuseStrategy,
|
|
await createState(config, 'a(left:b)'),
|
|
emptyState(),
|
|
);
|
|
advanceState(prevState);
|
|
const {state, newlyCreatedRoutes} = createRouterState(
|
|
reuseStrategy,
|
|
await createState(config, 'a(left:c)'),
|
|
prevState,
|
|
);
|
|
|
|
expect(prevState.root).toBe(state.root);
|
|
const prevC = (prevState as any).children(prevState.root);
|
|
const currC = (state as any).children(state.root);
|
|
|
|
expect(prevC[0]).toBe(currC[0]);
|
|
expect(prevC[1]).not.toBe(currC[1]);
|
|
checkActivatedRoute(currC[1], ComponentC, 'left');
|
|
|
|
expect(prevCreated.has(prevState.root)).toBe(false);
|
|
expect(prevCreated.has(prevC[0])).toBe(true);
|
|
expect(prevCreated.has(prevC[1])).toBe(true);
|
|
expect(prevCreated.size).toBe(2);
|
|
|
|
expect(newlyCreatedRoutes.has(state.root)).toBe(false);
|
|
expect(newlyCreatedRoutes.has(currC[0])).toBe(false);
|
|
expect(newlyCreatedRoutes.has(currC[1])).toBe(true);
|
|
expect(newlyCreatedRoutes.size).toBe(1);
|
|
});
|
|
|
|
it('should handle componentless routes', async () => {
|
|
const config = [
|
|
{
|
|
path: 'a/:id',
|
|
children: [
|
|
{path: 'b', component: ComponentA},
|
|
{path: 'c', component: ComponentB, outlet: 'right'},
|
|
],
|
|
},
|
|
];
|
|
|
|
const {state: prevState} = createRouterState(
|
|
reuseStrategy,
|
|
await createState(config, 'a/1;p=11/(b//right:c)'),
|
|
emptyState(),
|
|
);
|
|
advanceState(prevState);
|
|
const {state} = createRouterState(
|
|
reuseStrategy,
|
|
await createState(config, 'a/2;p=22/(b//right:c)'),
|
|
prevState,
|
|
);
|
|
|
|
expect(prevState.root).toBe(state.root);
|
|
const prevP = (prevState as any).firstChild(prevState.root)!;
|
|
const currP = (state as any).firstChild(state.root)!;
|
|
expect(prevP).toBe(currP);
|
|
|
|
const currC = (state as any).children(currP);
|
|
|
|
expect(currP._futureSnapshot.params).toEqual({id: '2', p: '22'});
|
|
expect(currP._futureSnapshot.paramMap.get('id')).toEqual('2');
|
|
expect(currP._futureSnapshot.paramMap.get('p')).toEqual('22');
|
|
checkActivatedRoute(currC[0], ComponentA);
|
|
checkActivatedRoute(currC[1], ComponentB, 'right');
|
|
});
|
|
|
|
it('should not retrieve routes when `shouldAttach` is always false', async () => {
|
|
const config: Routes = [
|
|
{path: 'a', component: ComponentA},
|
|
{path: 'b', component: ComponentB, outlet: 'left'},
|
|
{path: 'c', component: ComponentC, outlet: 'left'},
|
|
];
|
|
spyOn(reuseStrategy, 'retrieve');
|
|
|
|
const {state: prevState} = createRouterState(
|
|
reuseStrategy,
|
|
await createState(config, 'a(left:b)'),
|
|
emptyState(),
|
|
);
|
|
advanceState(prevState);
|
|
createRouterState(reuseStrategy, await createState(config, 'a(left:c)'), prevState);
|
|
expect(reuseStrategy.retrieve).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should consistently represent future and current state', async () => {
|
|
const config: Routes = [
|
|
{path: '', pathMatch: 'full', component: ComponentA},
|
|
{path: 'product/:id', component: ComponentB},
|
|
];
|
|
spyOn(reuseStrategy, 'shouldReuseRoute').and.callThrough();
|
|
const {state: previousState} = createRouterState(
|
|
reuseStrategy,
|
|
await createState(config, ''),
|
|
emptyState(),
|
|
);
|
|
advanceState(previousState);
|
|
(reuseStrategy.shouldReuseRoute as jasmine.Spy).calls.reset();
|
|
|
|
createRouterState(reuseStrategy, await createState(config, 'product/30'), previousState);
|
|
|
|
// One call for the root and one call for each of the children
|
|
expect(reuseStrategy.shouldReuseRoute).toHaveBeenCalledTimes(2);
|
|
const reuseCalls = (reuseStrategy.shouldReuseRoute as jasmine.Spy).calls;
|
|
const future1 = reuseCalls.argsFor(0)[0];
|
|
const current1 = reuseCalls.argsFor(0)[1];
|
|
const future2 = reuseCalls.argsFor(1)[0];
|
|
const current2 = reuseCalls.argsFor(1)[1];
|
|
|
|
// Routing from '' to 'product/30'
|
|
expect(current1._routerState.url).toEqual(tree('').toString());
|
|
expect(future1._routerState.url).toEqual(tree('product/30').toString());
|
|
expect(current2._routerState.url).toEqual(tree('').toString());
|
|
expect(future2._routerState.url).toEqual(tree('product/30').toString());
|
|
});
|
|
|
|
it('should not include attached routes in newlyCreatedRoutes', async () => {
|
|
const config = [{path: 'a', component: ComponentA}];
|
|
|
|
// First transition creates the route
|
|
const {state: state1, newlyCreatedRoutes: created1} = createRouterState(
|
|
reuseStrategy,
|
|
await createState(config, 'a'),
|
|
emptyState(),
|
|
);
|
|
const routeA = (state1 as any).firstChild(state1.root);
|
|
expect(created1.has(routeA)).toBe(true);
|
|
|
|
// Simulate detaching routeA
|
|
const detachedHandle = {
|
|
route: new TreeNode<ActivatedRoute>(routeA, []),
|
|
} as any;
|
|
|
|
spyOn(reuseStrategy, 'shouldAttach').and.returnValue(true);
|
|
spyOn(reuseStrategy, 'retrieve').and.returnValue(detachedHandle);
|
|
|
|
// Second transition attaches the route
|
|
const {state: state2, newlyCreatedRoutes: created2} = createRouterState(
|
|
reuseStrategy,
|
|
await createState(config, 'a'),
|
|
emptyState(),
|
|
);
|
|
|
|
const child2 = (state2 as any).firstChild(state2.root);
|
|
expect(child2).toBe(routeA); // attached route
|
|
expect(created2.has(routeA)).toBe(false); // not in newlyCreatedRoutes of the second transition
|
|
expect(created2.size).toBe(0);
|
|
});
|
|
});
|
|
|
|
function advanceState(state: RouterState): void {
|
|
advanceNode((state as any)._root);
|
|
}
|
|
|
|
function advanceNode(node: TreeNode<ActivatedRoute>): void {
|
|
advanceActivatedRoute(node.value);
|
|
node.children.forEach(advanceNode);
|
|
}
|
|
|
|
async function createState(config: Routes, url: string): Promise<RouterStateSnapshot> {
|
|
const result = await recognize(
|
|
TestBed.inject(EnvironmentInjector),
|
|
TestBed.inject(RouterConfigLoader),
|
|
RootComponent,
|
|
config,
|
|
tree(url),
|
|
new DefaultUrlSerializer(),
|
|
DEFAULT_PARAMS_INHERITANCE_STRATEGY,
|
|
new AbortController().signal,
|
|
);
|
|
return result.state;
|
|
}
|
|
|
|
function checkActivatedRoute(
|
|
actual: ActivatedRoute,
|
|
cmp: Function,
|
|
outlet: string = PRIMARY_OUTLET,
|
|
): void {
|
|
if (actual === null) {
|
|
expect(actual).toBeDefined();
|
|
} else {
|
|
expect(actual.component as any).toBe(cmp);
|
|
expect(actual.outlet).toEqual(outlet);
|
|
}
|
|
}
|
|
|
|
function tree(url: string): UrlTree {
|
|
return new DefaultUrlSerializer().parse(url);
|
|
}
|
|
|
|
class RootComponent {}
|
|
class ComponentA {}
|
|
class ComponentB {}
|
|
class ComponentC {}
|