77 */
88
99import { TestBed } from '@angular/core/testing' ;
10- import { NavigationStart , provideRouter , Event , Router } from '../src' ;
10+ import {
11+ NavigationStart ,
12+ provideRouter ,
13+ Event ,
14+ Router ,
15+ UrlSerializer ,
16+ DefaultUrlSerializer ,
17+ UrlTree ,
18+ Params ,
19+ } from '../src' ;
1120import { withExperimentalPlatformNavigation , withRouterConfig } from '../src/provide_router' ;
1221import { withBody , useAutoTick , timeout } from '@angular/private/testing' ;
1322import {
@@ -21,6 +30,7 @@ import {
2130 ɵFakeNavigation as FakeNavigation ,
2231 ɵFakeNavigationPlatformLocation as FakeNavigationPlatformLocation ,
2332 provideLocationMocks ,
33+ MOCK_PLATFORM_LOCATION_CONFIG ,
2434} from '@angular/common/testing' ;
2535import { inject } from '@angular/core' ;
2636
@@ -227,6 +237,152 @@ describe('withPlatformNavigation feature', () => {
227237 await expectAsync ( finished ) . toBeResolved ( ) ;
228238 } ) ;
229239 } ) ;
240+
241+ class TrailingSlashNormalizingUrlSerializer extends DefaultUrlSerializer {
242+ override parse ( url : string ) : UrlTree {
243+ if ( url !== '/' && url . endsWith ( '/' ) ) {
244+ url = url . slice ( 0 , - 1 ) ;
245+ }
246+ return super . parse ( url ) ;
247+ }
248+ override serialize ( tree : UrlTree ) : string {
249+ let url = super . serialize ( tree ) ;
250+ if ( url !== '/' && url . endsWith ( '/' ) ) {
251+ url = url . slice ( 0 , - 1 ) ;
252+ }
253+ return url ;
254+ }
255+ }
256+
257+ class QueryParamSortingUrlSerializer extends DefaultUrlSerializer {
258+ override parse ( url : string ) : UrlTree {
259+ const tree = super . parse ( url ) ;
260+ const sorted : Params = { } ;
261+ for ( const key of Object . keys ( tree . queryParams ) . sort ( ) ) {
262+ sorted [ key ] = tree . queryParams [ key ] ;
263+ }
264+ tree . queryParams = sorted ;
265+ return tree ;
266+ }
267+ override serialize ( tree : UrlTree ) : string {
268+ const sorted : Params = { } ;
269+ for ( const key of Object . keys ( tree . queryParams ) . sort ( ) ) {
270+ sorted [ key ] = tree . queryParams [ key ] ;
271+ }
272+ const newTree = new UrlTree ( tree . root , sorted , tree . fragment ) ;
273+ return super . serialize ( newTree ) ;
274+ }
275+ }
276+
277+ describe ( 'URL comparison and extraction bugs' , ( ) => {
278+ useAutoTick ( ) ;
279+ let router : Router ;
280+ let navigation : PlatformNavigation ;
281+
282+ it ( 'should not trigger new navigation when traversing back to a URL with trailing slash mismatch (with custom serializer)' , async ( ) => {
283+ TestBed . resetTestingModule ( ) ;
284+ TestBed . configureTestingModule ( {
285+ providers : [
286+ { provide : UrlSerializer , useClass : TrailingSlashNormalizingUrlSerializer } ,
287+ { provide : PRECOMMIT_HANDLER_SUPPORTED , useValue : false } ,
288+ provideRouter (
289+ [
290+ { path : 'foo' , children : [ ] } ,
291+ { path : 'bar' , children : [ ] } ,
292+ ] ,
293+ withExperimentalPlatformNavigation ( ) ,
294+ ) ,
295+ ] ,
296+ } ) ;
297+ navigation = TestBed . inject ( PlatformNavigation ) ;
298+
299+ navigation . navigate ( '/foo/' ) ;
300+ await timeout ( ) ;
301+ navigation . navigate ( '/bar' ) ;
302+ await timeout ( ) ;
303+
304+ router = TestBed . inject ( Router ) ;
305+ router . initialNavigation ( ) ;
306+ await navigation . transition ?. finished ;
307+
308+ const navigateSpy = spyOn ( navigation , 'navigate' ) . and . callThrough ( ) ;
309+
310+ await navigation . back ( ) . finished ;
311+ await timeout ( ) ;
312+
313+ expect ( navigateSpy ) . not . toHaveBeenCalled ( ) ;
314+ } ) ;
315+
316+ it ( 'should not trigger new navigation when traversing back to a URL with query param order mismatch (with custom serializer)' , async ( ) => {
317+ TestBed . resetTestingModule ( ) ;
318+ TestBed . configureTestingModule ( {
319+ providers : [
320+ { provide : UrlSerializer , useClass : QueryParamSortingUrlSerializer } ,
321+ { provide : PRECOMMIT_HANDLER_SUPPORTED , useValue : false } ,
322+ provideRouter (
323+ [
324+ { path : 'foo' , children : [ ] } ,
325+ { path : 'bar' , children : [ ] } ,
326+ ] ,
327+ withExperimentalPlatformNavigation ( ) ,
328+ ) ,
329+ ] ,
330+ } ) ;
331+ navigation = TestBed . inject ( PlatformNavigation ) ;
332+
333+ navigation . navigate ( '/foo?b=2&a=1' ) ;
334+ await timeout ( ) ;
335+ navigation . navigate ( '/bar' ) ;
336+ await timeout ( ) ;
337+
338+ router = TestBed . inject ( Router ) ;
339+ router . initialNavigation ( ) ;
340+ await navigation . transition ?. finished ;
341+
342+ const navigateSpy = spyOn ( navigation , 'navigate' ) . and . callThrough ( ) ;
343+
344+ await navigation . back ( ) . finished ;
345+ await timeout ( ) ;
346+
347+ expect ( navigateSpy ) . not . toHaveBeenCalled ( ) ;
348+ } ) ;
349+
350+ it ( 'should not intercept navigations outside the app root' , async ( ) => {
351+ TestBed . resetTestingModule ( ) ;
352+ TestBed . configureTestingModule ( {
353+ providers : [
354+ {
355+ provide : MOCK_PLATFORM_LOCATION_CONFIG ,
356+ useValue : {
357+ startUrl : 'http://localhost/my-app/' ,
358+ appBaseHref : '/my-app/' ,
359+ } ,
360+ } ,
361+ provideRouter ( [ { path : '**' , children : [ ] } ] , withExperimentalPlatformNavigation ( ) ) ,
362+ ] ,
363+ } ) ;
364+ navigation = TestBed . inject ( PlatformNavigation ) ;
365+
366+ let interceptCalled = false ;
367+ navigation . addEventListener ( 'navigate' , ( e : any ) => {
368+ const originalIntercept = e . intercept ;
369+ e . intercept = function ( ...args : any [ ] ) {
370+ interceptCalled = true ;
371+ originalIntercept . apply ( this , args ) ;
372+ } ;
373+ } ) ;
374+
375+ router = TestBed . inject ( Router ) ;
376+ router . initialNavigation ( ) ;
377+ await navigation . transition ?. finished ;
378+
379+ interceptCalled = false ;
380+ navigation . navigate ( 'http://localhost/other-app/foo' ) ;
381+ await timeout ( ) ;
382+
383+ expect ( interceptCalled ) . toBeFalse ( ) ;
384+ } ) ;
385+ } ) ;
230386} ) ;
231387
232388describe ( 'configuration error' , ( ) => {
0 commit comments