mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
a205de99ba
OnPush is now the default change detection strategy, so the explicit test configuration is no longer needed.
(cherry picked from commit a958d7fe30)
980 lines
32 KiB
TypeScript
980 lines
32 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 {Location} from '@angular/common';
|
|
import {
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
NgModule,
|
|
ɵConsole as Console,
|
|
makeEnvironmentProviders,
|
|
signal,
|
|
} from '@angular/core';
|
|
import {TestBed} from '@angular/core/testing';
|
|
import {expect} from '@angular/private/testing/matchers';
|
|
import {
|
|
ActivationEnd,
|
|
ActivationStart,
|
|
ChildActivationEnd,
|
|
ChildActivationStart,
|
|
Event,
|
|
GuardsCheckEnd,
|
|
GuardsCheckStart,
|
|
NavigationCancel,
|
|
NavigationEnd,
|
|
NavigationStart,
|
|
ResolveEnd,
|
|
ResolveStart,
|
|
Router,
|
|
RouterModule,
|
|
RoutesRecognized,
|
|
} from '../../index';
|
|
|
|
import {provideRouter, withExperimentalPlatformNavigation} from '../../src/provide_router';
|
|
import {
|
|
BlankCmp,
|
|
CollectParamsCmp,
|
|
ComponentRecordingRoutePathAndUrl,
|
|
EmptyQueryParamsCmp,
|
|
expectEvents,
|
|
onlyNavigationStartAndEnd,
|
|
OutletInNgIf,
|
|
QueryParamsAndFragmentCmp,
|
|
RootCmp,
|
|
RootCmpWithNamedOutlet,
|
|
RootCmpWithOnInit,
|
|
RootCmpWithTwoOutlets,
|
|
RouteCmp,
|
|
ROUTER_DIRECTIVES,
|
|
SimpleCmp,
|
|
TeamCmp,
|
|
TestModule,
|
|
TwoOutletsCmp,
|
|
UserCmp,
|
|
createRoot,
|
|
advance,
|
|
simulateLocationChange,
|
|
} from './integration_helpers';
|
|
import {guardsIntegrationSuite} from './guards.spec';
|
|
import {lazyLoadingIntegrationSuite} from './lazy_loading.spec';
|
|
import {routeDataIntegrationSuite} from './route_data.spec';
|
|
import {routeReuseIntegrationSuite} from './route_reuse_strategy.spec';
|
|
import {routerLinkActiveIntegrationSuite} from './router_link_active.spec';
|
|
import {routerEventsIntegrationSuite} from './router_events.spec';
|
|
import {redirectsIntegrationSuite} from './redirects.spec';
|
|
import {routerLinkIntegrationSpec} from './router_links.spec';
|
|
import {navigationIntegrationTestSuite} from './navigation.spec';
|
|
import {eagerUrlUpdateStrategyIntegrationSuite} from './eager_url_update_strategy.spec';
|
|
import {duplicateInFlightNavigationsIntegrationSuite} from './duplicate_in_flight_navigations.spec';
|
|
import {navigationErrorsIntegrationSuite} from './navigation_errors.spec';
|
|
import {useAutoTick} from '@angular/private/testing';
|
|
import {RouterTestingHarness} from '../../testing';
|
|
|
|
for (const browserAPI of ['navigation', 'history'] as const) {
|
|
describe(`${browserAPI}-based routing`, () => {
|
|
useAutoTick();
|
|
const noopConsole: Console = {log() {}, warn() {}};
|
|
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({
|
|
imports: [...ROUTER_DIRECTIVES, TestModule],
|
|
providers: [
|
|
{provide: Console, useValue: noopConsole},
|
|
provideRouter(
|
|
[{path: 'simple', component: SimpleCmp}],
|
|
browserAPI === 'navigation'
|
|
? withExperimentalPlatformNavigation()
|
|
: (makeEnvironmentProviders([]) as any),
|
|
),
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should navigate with a provided config', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.navigateByUrl('/simple');
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('/simple');
|
|
});
|
|
|
|
it('should navigate from ngOnInit hook', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
router.resetConfig([
|
|
{path: '', component: SimpleCmp},
|
|
{path: 'one', component: RouteCmp},
|
|
]);
|
|
|
|
const fixture = await createRoot(router, RootCmpWithOnInit);
|
|
expect(location.path()).toEqual('/one');
|
|
expect(fixture.nativeElement).toHaveText('route');
|
|
});
|
|
|
|
it('Should work inside ChangeDetectionStrategy.OnPush components', async () => {
|
|
@Component({
|
|
selector: 'root-cmp',
|
|
template: `<router-outlet></router-outlet>`,
|
|
standalone: false,
|
|
})
|
|
class OnPushOutlet {}
|
|
|
|
@Component({
|
|
selector: 'need-cd',
|
|
template: `{{ 'it works!' }}`,
|
|
standalone: false,
|
|
})
|
|
class NeedCdCmp {}
|
|
|
|
@NgModule({
|
|
declarations: [OnPushOutlet, NeedCdCmp],
|
|
imports: [RouterModule.forRoot([])],
|
|
})
|
|
class TestModule {}
|
|
|
|
TestBed.configureTestingModule({imports: [TestModule]});
|
|
|
|
const router: Router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'on',
|
|
component: OnPushOutlet,
|
|
children: [
|
|
{
|
|
path: 'push',
|
|
component: NeedCdCmp,
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
await advance(fixture);
|
|
router.navigateByUrl('on');
|
|
await advance(fixture);
|
|
router.navigateByUrl('on/push');
|
|
await advance(fixture);
|
|
|
|
expect(fixture.nativeElement).toHaveText('it works!');
|
|
});
|
|
|
|
it('should not error when no url left and no children are matching', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'team/:id',
|
|
component: TeamCmp,
|
|
children: [{path: 'simple', component: SimpleCmp}],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/33/simple');
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('/team/33/simple');
|
|
expect(fixture.nativeElement).toHaveText('team 33 [ simple, right: ]');
|
|
|
|
router.navigateByUrl('/team/33');
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('/team/33');
|
|
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
|
|
});
|
|
|
|
it('should work when an outlet is in an ngIf', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'child',
|
|
component: OutletInNgIf,
|
|
children: [{path: 'simple', component: SimpleCmp}],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/child/simple');
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('/child/simple');
|
|
});
|
|
|
|
it('should work when an outlet is added/removed', async () => {
|
|
@Component({
|
|
selector: 'someRoot',
|
|
template: `[
|
|
<div *ngIf="cond()"><router-outlet></router-outlet></div>
|
|
]`,
|
|
standalone: false,
|
|
})
|
|
class RootCmpWithLink {
|
|
cond = signal(true);
|
|
}
|
|
TestBed.configureTestingModule({declarations: [RootCmpWithLink]});
|
|
|
|
const router: Router = TestBed.inject(Router);
|
|
|
|
const fixture = await createRoot(router, RootCmpWithLink);
|
|
|
|
router.resetConfig([
|
|
{path: 'simple', component: SimpleCmp},
|
|
{path: 'blank', component: BlankCmp},
|
|
]);
|
|
|
|
router.navigateByUrl('/simple');
|
|
await advance(fixture);
|
|
expect(fixture.nativeElement).toHaveText('[ simple ]');
|
|
|
|
fixture.componentInstance.cond.set(false);
|
|
await advance(fixture);
|
|
expect(fixture.nativeElement).toHaveText('[ ]');
|
|
|
|
fixture.componentInstance.cond.set(true);
|
|
await advance(fixture);
|
|
expect(fixture.nativeElement).toHaveText('[ simple ]');
|
|
});
|
|
|
|
it('should update location when navigating', async () => {
|
|
@Component({
|
|
template: `record`,
|
|
standalone: false,
|
|
})
|
|
class RecordLocationCmp {
|
|
private storedPath: string;
|
|
constructor(loc: Location) {
|
|
this.storedPath = loc.path();
|
|
}
|
|
}
|
|
|
|
@NgModule({declarations: [RecordLocationCmp]})
|
|
class TestModule {}
|
|
|
|
TestBed.configureTestingModule({imports: [TestModule]});
|
|
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([{path: 'record/:id', component: RecordLocationCmp}]);
|
|
|
|
router.navigateByUrl('/record/22');
|
|
await advance(fixture);
|
|
|
|
const c = fixture.debugElement.children[1].componentInstance;
|
|
expect(location.path()).toEqual('/record/22');
|
|
expect(c.storedPath).toEqual('/record/22');
|
|
|
|
router.navigateByUrl('/record/33');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/record/33');
|
|
});
|
|
|
|
it('should skip location update when using NavigationExtras.skipLocationChange with navigateByUrl', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = TestBed.createComponent(RootCmp);
|
|
await advance(fixture);
|
|
|
|
router.resetConfig([{path: 'team/:id', component: TeamCmp}]);
|
|
|
|
router.navigateByUrl('/team/22');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/22');
|
|
|
|
expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]');
|
|
|
|
router.navigateByUrl('/team/33', {skipLocationChange: true});
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('/team/22');
|
|
|
|
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
|
|
});
|
|
|
|
it('should skip location update when using NavigationExtras.skipLocationChange with navigate', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = TestBed.createComponent(RootCmp);
|
|
await advance(fixture);
|
|
|
|
router.resetConfig([{path: 'team/:id', component: TeamCmp}]);
|
|
|
|
router.navigate(['/team/22']);
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/22');
|
|
|
|
expect(fixture.nativeElement).toHaveText('team 22 [ , right: ]');
|
|
|
|
router.navigate(['/team/33'], {skipLocationChange: true});
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('/team/22');
|
|
|
|
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
|
|
});
|
|
|
|
it('should navigate after navigation with skipLocationChange', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = TestBed.createComponent(RootCmpWithNamedOutlet);
|
|
await advance(fixture);
|
|
|
|
router.resetConfig([{path: 'show', outlet: 'main', component: SimpleCmp}]);
|
|
|
|
router.navigate([{outlets: {main: 'show'}}], {skipLocationChange: true});
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('');
|
|
|
|
expect(fixture.nativeElement).toHaveText('main [simple]');
|
|
|
|
router.navigate([{outlets: {main: null}}], {skipLocationChange: true});
|
|
await advance(fixture);
|
|
|
|
expect(location.path()).toEqual('');
|
|
|
|
expect(fixture.nativeElement).toHaveText('main []');
|
|
});
|
|
|
|
it('should navigate back and forward', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'team/:id',
|
|
component: TeamCmp,
|
|
children: [
|
|
{path: 'simple', component: SimpleCmp},
|
|
{path: 'user/:name', component: UserCmp},
|
|
],
|
|
},
|
|
]);
|
|
|
|
let event: NavigationStart;
|
|
router.events.subscribe((e) => {
|
|
if (e instanceof NavigationStart) {
|
|
event = e;
|
|
}
|
|
});
|
|
|
|
router.navigateByUrl('/team/33/simple');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/33/simple');
|
|
const simpleNavStart = event!;
|
|
|
|
router.navigateByUrl('/team/22/user/victor');
|
|
await advance(fixture);
|
|
const userVictorNavStart = event!;
|
|
|
|
location.back();
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/33/simple');
|
|
expect(event!.navigationTrigger).toEqual('popstate');
|
|
expect(event!.restoredState!.navigationId).toEqual(simpleNavStart.id);
|
|
|
|
location.forward();
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/22/user/victor');
|
|
expect(event!.navigationTrigger).toEqual('popstate');
|
|
expect(event!.restoredState!.navigationId).toEqual(userVictorNavStart.id);
|
|
});
|
|
|
|
it('should restore internal route on popstate when browserUrl is used', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
|
|
router.resetConfig([
|
|
{path: 'home', component: SimpleCmp},
|
|
{path: 'one', component: SimpleCmp},
|
|
]);
|
|
|
|
const harness = await RouterTestingHarness.create('/home');
|
|
router.setUpLocationChangeListener();
|
|
|
|
await router.navigateByUrl('/one', {browserUrl: '/display-one'});
|
|
expect(location.path()).toEqual('/display-one');
|
|
expect(router.url).toEqual('/one');
|
|
|
|
location.back();
|
|
await advance(harness.fixture);
|
|
expect(location.path()).toEqual('/home');
|
|
expect(router.url).toEqual('/home');
|
|
|
|
location.forward();
|
|
await advance(harness.fixture);
|
|
expect(router.url).toEqual('/one');
|
|
expect(location.path()).toEqual('/display-one');
|
|
});
|
|
|
|
it('should navigate to the same url when config changes', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([{path: 'a', component: SimpleCmp}]);
|
|
|
|
router.navigate(['/a']);
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/a');
|
|
expect(fixture.nativeElement).toHaveText('simple');
|
|
|
|
router.resetConfig([{path: 'a', component: RouteCmp}]);
|
|
|
|
router.navigate(['/a']);
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/a');
|
|
expect(fixture.nativeElement).toHaveText('route');
|
|
});
|
|
|
|
it('should navigate when locations changes', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'team/:id',
|
|
component: TeamCmp,
|
|
children: [{path: 'user/:name', component: UserCmp}],
|
|
},
|
|
]);
|
|
|
|
const recordedEvents: (NavigationStart | NavigationEnd)[] = [];
|
|
router.events.forEach((e) => onlyNavigationStartAndEnd(e) && recordedEvents.push(e));
|
|
|
|
router.navigateByUrl('/team/22/user/victor');
|
|
await advance(fixture);
|
|
|
|
simulateLocationChange('/team/22/user/fedor', browserAPI);
|
|
await advance(fixture);
|
|
|
|
simulateLocationChange('/team/22/user/fedor', browserAPI);
|
|
await advance(fixture);
|
|
|
|
expect(fixture.nativeElement).toHaveText('team 22 [ user fedor, right: ]');
|
|
|
|
expectEvents(recordedEvents, [
|
|
[NavigationStart, '/team/22/user/victor'],
|
|
[NavigationEnd, '/team/22/user/victor'],
|
|
[NavigationStart, '/team/22/user/fedor'],
|
|
[NavigationEnd, '/team/22/user/fedor'],
|
|
]);
|
|
});
|
|
|
|
it('should update the location when the matched route does not change', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([{path: '**', component: CollectParamsCmp}]);
|
|
|
|
router.navigateByUrl('/one/two');
|
|
await advance(fixture);
|
|
const cmp = fixture.debugElement.children[1].componentInstance;
|
|
expect(location.path()).toEqual('/one/two');
|
|
expect(fixture.nativeElement).toHaveText('collect-params');
|
|
|
|
expect(cmp.recordedUrls()).toEqual(['one/two']);
|
|
|
|
router.navigateByUrl('/three/four');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/three/four');
|
|
expect(fixture.nativeElement).toHaveText('collect-params');
|
|
expect(cmp.recordedUrls()).toEqual(['one/two', 'three/four']);
|
|
});
|
|
|
|
it('should support secondary routes', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'team/:id',
|
|
component: TeamCmp,
|
|
children: [
|
|
{path: 'user/:name', component: UserCmp},
|
|
{path: 'simple', component: SimpleCmp, outlet: 'right'},
|
|
],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/22/(user/victor//right:simple)');
|
|
await advance(fixture);
|
|
|
|
expect(fixture.nativeElement).toHaveText('team 22 [ user victor, right: simple ]');
|
|
});
|
|
|
|
it('should support secondary routes in separate commands', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'team/:id',
|
|
component: TeamCmp,
|
|
children: [
|
|
{path: 'user/:name', component: UserCmp},
|
|
{path: 'simple', component: SimpleCmp, outlet: 'right'},
|
|
],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/22/user/victor');
|
|
await advance(fixture);
|
|
router.navigate(['team/22', {outlets: {right: 'simple'}}]);
|
|
await advance(fixture);
|
|
|
|
expect(fixture.nativeElement).toHaveText('team 22 [ user victor, right: simple ]');
|
|
});
|
|
|
|
it('should support secondary routes as child of empty path parent', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: '',
|
|
component: TeamCmp,
|
|
children: [{path: 'simple', component: SimpleCmp, outlet: 'right'}],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/(right:simple)');
|
|
await advance(fixture);
|
|
|
|
expect(fixture.nativeElement).toHaveText('team [ , right: simple ]');
|
|
});
|
|
|
|
it('should deactivate outlets', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'team/:id',
|
|
component: TeamCmp,
|
|
children: [
|
|
{path: 'user/:name', component: UserCmp},
|
|
{path: 'simple', component: SimpleCmp, outlet: 'right'},
|
|
],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/22/(user/victor//right:simple)');
|
|
await advance(fixture);
|
|
|
|
router.navigateByUrl('/team/22/user/victor');
|
|
await advance(fixture);
|
|
|
|
expect(fixture.nativeElement).toHaveText('team 22 [ user victor, right: ]');
|
|
});
|
|
|
|
it('should deactivate nested outlets', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'team/:id',
|
|
component: TeamCmp,
|
|
children: [
|
|
{path: 'user/:name', component: UserCmp},
|
|
{path: 'simple', component: SimpleCmp, outlet: 'right'},
|
|
],
|
|
},
|
|
{path: '', component: BlankCmp},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/22/(user/victor//right:simple)');
|
|
await advance(fixture);
|
|
|
|
router.navigateByUrl('/');
|
|
await advance(fixture);
|
|
|
|
expect(fixture.nativeElement).toHaveText('');
|
|
});
|
|
|
|
it('should set query params and fragment', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([{path: 'query', component: QueryParamsAndFragmentCmp}]);
|
|
|
|
router.navigateByUrl('/query?name=1#fragment1');
|
|
await advance(fixture);
|
|
expect(fixture.nativeElement).toHaveText('query: 1 fragment: fragment1');
|
|
|
|
router.navigateByUrl('/query?name=2#fragment2');
|
|
await advance(fixture);
|
|
expect(fixture.nativeElement).toHaveText('query: 2 fragment: fragment2');
|
|
});
|
|
|
|
it('should handle empty or missing fragments', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([{path: 'query', component: QueryParamsAndFragmentCmp}]);
|
|
|
|
router.navigateByUrl('/query#');
|
|
await advance(fixture);
|
|
expect(fixture.nativeElement).toHaveText('query: fragment: ');
|
|
|
|
router.navigateByUrl('/query');
|
|
await advance(fixture);
|
|
expect(fixture.nativeElement).toHaveText('query: fragment: null');
|
|
});
|
|
|
|
it('should ignore null and undefined query params', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([{path: 'query', component: EmptyQueryParamsCmp}]);
|
|
|
|
router.navigate(['query'], {queryParams: {name: 1, age: null, page: undefined}});
|
|
await advance(fixture);
|
|
const cmp = fixture.debugElement.children[1].componentInstance;
|
|
expect(cmp.recordedParams).toEqual([{name: '1'}]);
|
|
});
|
|
|
|
it('should push params only when they change', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'team/:id',
|
|
component: TeamCmp,
|
|
children: [{path: 'user/:name', component: UserCmp}],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/22/user/victor');
|
|
await advance(fixture);
|
|
const team = fixture.debugElement.children[1].componentInstance;
|
|
const user = fixture.debugElement.children[1].children[1].componentInstance;
|
|
|
|
expect(team.recordedParams).toEqual([{id: '22'}]);
|
|
expect(team.snapshotParams).toEqual([{id: '22'}]);
|
|
expect(user.recordedParams).toEqual([{id: '22', name: 'victor'}]);
|
|
expect(user.snapshotParams).toEqual([{id: '22', name: 'victor'}]);
|
|
|
|
router.navigateByUrl('/team/22/user/fedor');
|
|
await advance(fixture);
|
|
|
|
expect(team.recordedParams).toEqual([{id: '22'}]);
|
|
expect(team.snapshotParams).toEqual([{id: '22'}]);
|
|
expect(user.recordedParams).toEqual([
|
|
{id: '22', name: 'victor'},
|
|
{id: '22', name: 'fedor'},
|
|
]);
|
|
expect(user.snapshotParams).toEqual([
|
|
{id: '22', name: 'victor'},
|
|
{id: '22', name: 'fedor'},
|
|
]);
|
|
});
|
|
|
|
it('should work when navigating to /', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{path: '', pathMatch: 'full', component: SimpleCmp},
|
|
{path: 'user/:name', component: UserCmp},
|
|
]);
|
|
|
|
router.navigateByUrl('/user/victor');
|
|
await advance(fixture);
|
|
|
|
expect(fixture.nativeElement).toHaveText('user victor');
|
|
|
|
router.navigateByUrl('/');
|
|
await advance(fixture);
|
|
|
|
expect(fixture.nativeElement).toHaveText('simple');
|
|
});
|
|
|
|
it('should cancel in-flight navigations', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([{path: 'user/:name', component: UserCmp}]);
|
|
|
|
const recordedEvents: Event[] = [];
|
|
router.events.forEach((e) => recordedEvents.push(e));
|
|
|
|
router.navigateByUrl('/user/init');
|
|
await advance(fixture);
|
|
|
|
const user = fixture.debugElement.children[1].componentInstance;
|
|
|
|
let r1: any, r2: any;
|
|
router.navigateByUrl('/user/victor').then((_) => (r1 = _));
|
|
router.navigateByUrl('/user/fedor').then((_) => (r2 = _));
|
|
await advance(fixture);
|
|
|
|
expect(r1).toEqual(false); // returns false because it was canceled
|
|
expect(r2).toEqual(true); // returns true because it was successful
|
|
|
|
expect(fixture.nativeElement).toHaveText('user fedor');
|
|
expect(user.recordedParams).toEqual([{name: 'init'}, {name: 'fedor'}]);
|
|
|
|
expectEvents(recordedEvents, [
|
|
[NavigationStart, '/user/init'],
|
|
[RoutesRecognized, '/user/init'],
|
|
[GuardsCheckStart, '/user/init'],
|
|
[ChildActivationStart],
|
|
[ActivationStart],
|
|
[GuardsCheckEnd, '/user/init'],
|
|
[ResolveStart, '/user/init'],
|
|
[ResolveEnd, '/user/init'],
|
|
[ActivationEnd],
|
|
[ChildActivationEnd],
|
|
[NavigationEnd, '/user/init'],
|
|
|
|
[NavigationStart, '/user/victor'],
|
|
[NavigationCancel, '/user/victor'],
|
|
|
|
[NavigationStart, '/user/fedor'],
|
|
[RoutesRecognized, '/user/fedor'],
|
|
[GuardsCheckStart, '/user/fedor'],
|
|
[ChildActivationStart],
|
|
[ActivationStart],
|
|
[GuardsCheckEnd, '/user/fedor'],
|
|
[ResolveStart, '/user/fedor'],
|
|
[ResolveEnd, '/user/fedor'],
|
|
[ActivationEnd],
|
|
[ChildActivationEnd],
|
|
[NavigationEnd, '/user/fedor'],
|
|
]);
|
|
});
|
|
|
|
it('should properly set currentNavigation when cancelling in-flight navigations', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([{path: 'user/:name', component: UserCmp}]);
|
|
|
|
router.navigateByUrl('/user/init');
|
|
await advance(fixture);
|
|
|
|
router.navigateByUrl('/user/victor');
|
|
expect(router.getCurrentNavigation()).not.toBe(null);
|
|
expect(router.currentNavigation()).not.toBe(null);
|
|
router.navigateByUrl('/user/fedor');
|
|
// Due to https://github.com/angular/angular/issues/29389, this would be `false`
|
|
// when running a second navigation.
|
|
expect(router.getCurrentNavigation()).not.toBe(null);
|
|
expect(router.currentNavigation()).not.toBe(null);
|
|
await advance(fixture);
|
|
|
|
expect(router.getCurrentNavigation()).toBe(null);
|
|
expect(router.currentNavigation()).toBe(null);
|
|
expect(fixture.nativeElement).toHaveText('user fedor');
|
|
});
|
|
|
|
it('should set LastSuccessfulNavigation', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const fixture = TestBed.createComponent(RootCmp);
|
|
router.resetConfig([{path: 'user/:name', component: UserCmp}]);
|
|
|
|
expect(router.lastSuccessfulNavigation()).toBe(null);
|
|
|
|
router.navigateByUrl('/user/init');
|
|
const navigation = router.currentNavigation();
|
|
expect(router.lastSuccessfulNavigation()).toBe(null);
|
|
await advance(fixture);
|
|
|
|
expect(router.currentNavigation()).toBe(null);
|
|
expect(router.lastSuccessfulNavigation()).toEqual(navigation);
|
|
});
|
|
|
|
it('should replace state when path is equal to current path', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'team/:id',
|
|
component: TeamCmp,
|
|
children: [
|
|
{path: 'simple', component: SimpleCmp},
|
|
{path: 'user/:name', component: UserCmp},
|
|
],
|
|
},
|
|
]);
|
|
|
|
router.navigateByUrl('/team/33/simple');
|
|
await advance(fixture);
|
|
|
|
router.navigateByUrl('/team/22/user/victor');
|
|
await advance(fixture);
|
|
|
|
router.navigateByUrl('/team/22/user/victor');
|
|
await advance(fixture);
|
|
|
|
location.back();
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/team/33/simple');
|
|
});
|
|
|
|
it('should handle componentless paths', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, RootCmpWithTwoOutlets);
|
|
|
|
router.resetConfig([
|
|
{
|
|
path: 'parent/:id',
|
|
children: [
|
|
{path: 'simple', component: SimpleCmp},
|
|
{path: 'user/:name', component: UserCmp, outlet: 'right'},
|
|
],
|
|
},
|
|
{path: 'user/:name', component: UserCmp},
|
|
]);
|
|
|
|
// navigate to a componentless route
|
|
router.navigateByUrl('/parent/11/(simple//right:user/victor)');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/parent/11/(simple//right:user/victor)');
|
|
expect(fixture.nativeElement).toHaveText('primary [simple] right [user victor]');
|
|
|
|
// navigate to the same route with different params (reuse)
|
|
router.navigateByUrl('/parent/22/(simple//right:user/fedor)');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/parent/22/(simple//right:user/fedor)');
|
|
expect(fixture.nativeElement).toHaveText('primary [simple] right [user fedor]');
|
|
|
|
// navigate to a normal route (check deactivation)
|
|
router.navigateByUrl('/user/victor');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/user/victor');
|
|
expect(fixture.nativeElement).toHaveText('primary [user victor] right []');
|
|
|
|
// navigate back to a componentless route
|
|
router.navigateByUrl('/parent/11/(simple//right:user/victor)');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/parent/11/(simple//right:user/victor)');
|
|
expect(fixture.nativeElement).toHaveText('primary [simple] right [user victor]');
|
|
});
|
|
|
|
it('should not deactivate aux routes when navigating from a componentless routes', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const location: Location = TestBed.inject(Location);
|
|
const fixture = await createRoot(router, TwoOutletsCmp);
|
|
|
|
router.resetConfig([
|
|
{path: 'simple', component: SimpleCmp},
|
|
{path: 'componentless', children: [{path: 'simple', component: SimpleCmp}]},
|
|
{path: 'user/:name', outlet: 'aux', component: UserCmp},
|
|
]);
|
|
|
|
router.navigateByUrl('/componentless/simple(aux:user/victor)');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/componentless/simple(aux:user/victor)');
|
|
expect(fixture.nativeElement).toHaveText('[ simple, aux: user victor ]');
|
|
|
|
router.navigateByUrl('/simple(aux:user/victor)');
|
|
await advance(fixture);
|
|
expect(location.path()).toEqual('/simple(aux:user/victor)');
|
|
expect(fixture.nativeElement).toHaveText('[ simple, aux: user victor ]');
|
|
});
|
|
|
|
it('should emit an event when an outlet gets activated', async () => {
|
|
@Component({
|
|
selector: 'container',
|
|
template: `<router-outlet
|
|
(activate)="recordActivate($event)"
|
|
(deactivate)="recordDeactivate($event)"
|
|
></router-outlet>`,
|
|
standalone: false,
|
|
})
|
|
class Container {
|
|
activations: any[] = [];
|
|
deactivations: any[] = [];
|
|
|
|
recordActivate(component: any): void {
|
|
this.activations.push(component);
|
|
}
|
|
|
|
recordDeactivate(component: any): void {
|
|
this.deactivations.push(component);
|
|
}
|
|
}
|
|
|
|
TestBed.configureTestingModule({declarations: [Container]});
|
|
|
|
const router: Router = TestBed.inject(Router);
|
|
|
|
const fixture = await createRoot(router, Container);
|
|
const cmp = fixture.componentInstance;
|
|
|
|
router.resetConfig([
|
|
{path: 'blank', component: BlankCmp},
|
|
{path: 'simple', component: SimpleCmp},
|
|
]);
|
|
|
|
cmp.activations = [];
|
|
cmp.deactivations = [];
|
|
|
|
router.navigateByUrl('/blank');
|
|
await advance(fixture);
|
|
|
|
expect(cmp.activations.length).toEqual(1);
|
|
expect(cmp.activations[0] instanceof BlankCmp).toBe(true);
|
|
|
|
router.navigateByUrl('/simple');
|
|
await advance(fixture);
|
|
|
|
expect(cmp.activations.length).toEqual(2);
|
|
expect(cmp.activations[1] instanceof SimpleCmp).toBe(true);
|
|
expect(cmp.deactivations.length).toEqual(1);
|
|
expect(cmp.deactivations[0] instanceof BlankCmp).toBe(true);
|
|
});
|
|
|
|
it('should update url and router state before activating components', async () => {
|
|
const router: Router = TestBed.inject(Router);
|
|
const fixture = await createRoot(router, RootCmp);
|
|
|
|
router.resetConfig([{path: 'cmp', component: ComponentRecordingRoutePathAndUrl}]);
|
|
|
|
router.navigateByUrl('/cmp');
|
|
await advance(fixture);
|
|
|
|
const cmp: ComponentRecordingRoutePathAndUrl =
|
|
fixture.debugElement.children[1].componentInstance;
|
|
|
|
expect(cmp.url).toBe('/cmp');
|
|
expect(cmp.path.length).toEqual(2);
|
|
});
|
|
|
|
navigationErrorsIntegrationSuite(browserAPI);
|
|
eagerUrlUpdateStrategyIntegrationSuite();
|
|
duplicateInFlightNavigationsIntegrationSuite(browserAPI);
|
|
navigationIntegrationTestSuite(browserAPI);
|
|
routeDataIntegrationSuite();
|
|
routerLinkIntegrationSpec();
|
|
redirectsIntegrationSuite(browserAPI);
|
|
guardsIntegrationSuite();
|
|
routerEventsIntegrationSuite();
|
|
routerLinkActiveIntegrationSuite();
|
|
lazyLoadingIntegrationSuite(browserAPI);
|
|
routeReuseIntegrationSuite();
|
|
});
|
|
}
|