@@ -12,6 +12,7 @@ import {ComponentFixture, TestBed, fakeAsync, inject, tick} from '@angular/core/
1212import { By } from '@angular/platform-browser/src/dom/debug/by' ;
1313import { expect } from '@angular/platform-browser/testing/src/matchers' ;
1414import { ActivatedRoute , ActivatedRouteSnapshot , ActivationEnd , ActivationStart , CanActivate , CanDeactivate , ChildActivationEnd , ChildActivationStart , DetachedRouteHandle , Event , GuardsCheckEnd , GuardsCheckStart , NavigationCancel , NavigationEnd , NavigationError , NavigationStart , PRIMARY_OUTLET , ParamMap , Params , PreloadAllModules , PreloadingStrategy , Resolve , ResolveEnd , ResolveStart , RouteConfigLoadEnd , RouteConfigLoadStart , RouteReuseStrategy , Router , RouterEvent , RouterModule , RouterPreloader , RouterStateSnapshot , RoutesRecognized , RunGuardsAndResolvers , UrlHandlingStrategy , UrlSegmentGroup , UrlTree } from '@angular/router' ;
15+ import { SpyLocation } from 'common/testing' ;
1516import { Observable } from 'rxjs/Observable' ;
1617import { Observer } from 'rxjs/Observer' ;
1718import { of } from 'rxjs/observable/of' ;
@@ -50,28 +51,87 @@ describe('Integration', () => {
5051 expect ( fixture . nativeElement ) . toHaveText ( 'route' ) ;
5152 } ) ) ) ;
5253
53- it ( 'should navigate to the current URL' ,
54- fakeAsync ( inject ( [ Router , Location ] , ( router : Router ) => {
55- router . resetConfig ( [
56- { path : '' , component : SimpleCmp } ,
57- { path : 'simple' , component : SimpleCmp } ,
58- ] ) ;
54+ describe ( 'navigation' , function ( ) {
55+ it ( 'should navigate to the current URL' , fakeAsync ( inject ( [ Router ] , ( router : Router ) => {
56+ router . resetConfig ( [
57+ { path : '' , component : SimpleCmp } ,
58+ { path : 'simple' , component : SimpleCmp } ,
59+ ] ) ;
5960
60- const fixture = createRoot ( router , RootCmp ) ;
61- const events : Event [ ] = [ ] ;
62- router . events . subscribe ( e => onlyNavigationStartAndEnd ( e ) && events . push ( e ) ) ;
61+ const fixture = createRoot ( router , RootCmp ) ;
62+ const events : Event [ ] = [ ] ;
63+ router . events . subscribe ( e => onlyNavigationStartAndEnd ( e ) && events . push ( e ) ) ;
6364
64- router . navigateByUrl ( '/simple' ) ;
65- tick ( ) ;
65+ router . navigateByUrl ( '/simple' ) ;
66+ tick ( ) ;
6667
67- router . navigateByUrl ( '/simple' ) ;
68- tick ( ) ;
68+ router . navigateByUrl ( '/simple' ) ;
69+ tick ( ) ;
6970
70- expectEvents ( events , [
71- [ NavigationStart , '/simple' ] , [ NavigationEnd , '/simple' ] , [ NavigationStart , '/simple' ] ,
72- [ NavigationEnd , '/simple' ]
73- ] ) ;
74- } ) ) ) ;
71+ expectEvents ( events , [
72+ [ NavigationStart , '/simple' ] , [ NavigationEnd , '/simple' ] , [ NavigationStart , '/simple' ] ,
73+ [ NavigationEnd , '/simple' ]
74+ ] ) ;
75+ } ) ) ) ;
76+
77+ it ( 'should not pollute browser history when replaceUrl is set to true' ,
78+ fakeAsync ( inject ( [ Router , Location ] , ( router : Router , location : SpyLocation ) => {
79+ router . resetConfig ( [
80+ { path : '' , component : SimpleCmp } , { path : 'a' , component : SimpleCmp } ,
81+ { path : 'b' , component : SimpleCmp }
82+ ] ) ;
83+
84+ const fixture = createRoot ( router , RootCmp ) ;
85+
86+ router . navigateByUrl ( '/a' , { replaceUrl : true } ) ;
87+ router . navigateByUrl ( '/b' , { replaceUrl : true } ) ;
88+ tick ( ) ;
89+
90+ expect ( location . urlChanges ) . toEqual ( [ 'replace: /' , 'replace: /b' ] ) ;
91+ } ) ) ) ;
92+
93+ it ( 'should skip navigation if another navigation is already scheduled' ,
94+ fakeAsync ( inject ( [ Router , Location ] , ( router : Router , location : SpyLocation ) => {
95+ router . resetConfig ( [
96+ { path : '' , component : SimpleCmp } , { path : 'a' , component : SimpleCmp } ,
97+ { path : 'b' , component : SimpleCmp }
98+ ] ) ;
99+
100+ const fixture = createRoot ( router , RootCmp ) ;
101+
102+ router . navigate (
103+ [ '/a' ] , { queryParams : { a : true } , queryParamsHandling : 'merge' , replaceUrl : true } ) ;
104+ router . navigate (
105+ [ '/b' ] , { queryParams : { b : true } , queryParamsHandling : 'merge' , replaceUrl : true } ) ;
106+ tick ( ) ;
107+
108+ /**
109+ * Why do we have '/b?b=true' and not '/b?a=true&b=true'?
110+ *
111+ * This is because the router has the right to stop a navigation mid-flight if another
112+ * navigation has been already scheduled. This is why we can use a top-level guard
113+ * to perform redirects. Calling `navigate` in such a guard will stop the navigation, and
114+ * the components won't be instantiated.
115+ *
116+ * This is a fundamental property of the router: it only cares about its latest state.
117+ *
118+ * This means that components should only map params to something else, not reduce them.
119+ * In other words, the following component is asking for trouble:
120+ *
121+ * ```
122+ * class MyComponent {
123+ * constructor(a: ActivatedRoute) {
124+ * a.params.scan(...)
125+ * }
126+ * }
127+ * ```
128+ *
129+ * This also means "queryParamsHandling: 'merge'" should only be used to merge with
130+ * long-living query parameters (e.g., debug).
131+ */
132+ expect ( router . url ) . toEqual ( '/b?b=true' ) ;
133+ } ) ) ) ;
134+ } ) ;
75135
76136 describe ( 'should execute navigations serially' , ( ) => {
77137 let log : any [ ] = [ ] ;
0 commit comments