3817e5f1df
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
177 lines
6.3 KiB
TypeScript
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 {}
|