angular-cn/packages/router/test/create_router_state.spec.ts
Mathieu Lemoine 3817e5f1df fix(router): Fix arguments order for call to shouldReuseRoute (#26949)
The `createOrReuseChildren` function calls shouldReuseRoute with the
previous child values use as the future and the future child value used
as the current argument. This is incosistent with the argument order in
`createNode`. This inconsistent order can make it difficult/impossible
to correctly implement the `shouldReuseRoute` function. Usually this
order doesn't matter because simple equality checks are made on the
args and it doesn't matter which is which.

More detail can be found in the bug report: #16192.

Fix #16192

BREAKING CHANGE: This change corrects the argument order when calling
RouteReuseStrategy#shouldReuseRoute. Previously, when evaluating child
routes, they would be called with the future and current arguments would
be swapped. If your RouteReuseStrategy relies specifically on only the future
or current snapshot state, you may need to update the shouldReuseRoute
implementation's use of "future" and "current" ActivateRouteSnapshots.

PR Close #26949
2020-09-15 11:33:52 -07:00

177 lines
6.3 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.io/license
*/
import {Routes} from '../src/config';
import {createRouterState} from '../src/create_router_state';
import {recognize} from '../src/recognize';
import {DefaultRouteReuseStrategy} from '../src/route_reuse_strategy';
import {ActivatedRoute, advanceActivatedRoute, createEmptyState, RouterState, RouterStateSnapshot} from '../src/router_state';
import {PRIMARY_OUTLET} from '../src/shared';
import {DefaultUrlSerializer, UrlSegmentGroup, 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(new (UrlTree as any)(new UrlSegmentGroup([], {}), {}, null!), RootComponent);
it('should create new state', () => {
const state = createRouterState(
reuseStrategy,
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');
});
it('should reuse existing nodes when it can', () => {
const config = [
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'},
{path: 'c', component: ComponentC, outlet: 'left'}
];
const prevState =
createRouterState(reuseStrategy, createState(config, 'a(left:b)'), emptyState());
advanceState(prevState);
const state = createRouterState(reuseStrategy, 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');
});
it('should handle componentless routes', () => {
const config = [{
path: 'a/:id',
children:
[{path: 'b', component: ComponentA}, {path: 'c', component: ComponentB, outlet: 'right'}]
}];
const prevState = createRouterState(
reuseStrategy, createState(config, 'a/1;p=11/(b//right:c)'), emptyState());
advanceState(prevState);
const state =
createRouterState(reuseStrategy, 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 cache the retrieved routeReuseStrategy', () => {
const config = [
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'},
{path: 'c', component: ComponentC, outlet: 'left'}
];
spyOn(reuseStrategy, 'retrieve').and.callThrough();
const prevState =
createRouterState(reuseStrategy, createState(config, 'a(left:b)'), emptyState());
advanceState(prevState);
// Expect 2 calls as the baseline setup
expect(reuseStrategy.retrieve).toHaveBeenCalledTimes(2);
// This call should produce a reused activated route
const state = createRouterState(reuseStrategy, createState(config, 'a(left:c)'), prevState);
// Verify the retrieve method has been called one more time
expect(reuseStrategy.retrieve).toHaveBeenCalledTimes(3);
});
it('should consistently represent future and current state', () => {
const config = [
{path: '', pathMatch: 'full', component: ComponentA},
{path: 'product/:id', component: ComponentB}
];
spyOn(reuseStrategy, 'shouldReuseRoute').and.callThrough();
const previousState = createRouterState(reuseStrategy, createState(config, ''), emptyState());
advanceState(previousState);
(reuseStrategy.shouldReuseRoute as jasmine.Spy).calls.reset();
createRouterState(reuseStrategy, 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('');
expect(future1._routerState.url).toEqual('product/30');
expect(current2._routerState.url).toEqual('');
expect(future2._routerState.url).toEqual('product/30');
});
});
function advanceState(state: RouterState): void {
advanceNode((state as any)._root);
}
function advanceNode(node: TreeNode<ActivatedRoute>): void {
advanceActivatedRoute(node.value);
node.children.forEach(advanceNode);
}
function createState(config: Routes, url: string): RouterStateSnapshot {
let res: RouterStateSnapshot = undefined!;
recognize(RootComponent, config, tree(url), url).forEach(s => res = s);
return res;
}
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 {}