fix(router): ensure duplicate popstate/hashchange events are handled correctly (#37674)
The current method of handling duplicate navigations caused by 'hashchange' and 'popstate' events for the same url change does not correctly handle cancelled navigations. Because `scheduleNavigation` is called in a `setTimeout` in the location change subscription, the duplicate navigations are not flushed at the same time. This means that if the initial navigation hits a guard that schedules a new navigation, the navigation for the duplicate event will not compare to the correct transition (because we inserted another navigation between the duplicates). See https://github.com/angular/angular/issues/16710#issuecomment-646919529 Fixes #16710 PR Close #37674
This commit is contained in:
@ -910,17 +910,29 @@ describe('Integration', () => {
|
||||
})));
|
||||
|
||||
describe('duplicate in-flight navigations', () => {
|
||||
@Injectable()
|
||||
class RedirectingGuard {
|
||||
constructor(private router: Router) {}
|
||||
canActivate() {
|
||||
this.router.navigate(['/simple']);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{
|
||||
provide: 'in1Second',
|
||||
useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => {
|
||||
let res: any = null;
|
||||
const p = new Promise(_ => res = _);
|
||||
setTimeout(() => res(true), 1000);
|
||||
return p;
|
||||
}
|
||||
}]
|
||||
providers: [
|
||||
{
|
||||
provide: 'in1Second',
|
||||
useValue: (c: any, a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => {
|
||||
let res: any = null;
|
||||
const p = new Promise(_ => res = _);
|
||||
setTimeout(() => res(true), 1000);
|
||||
return p;
|
||||
}
|
||||
},
|
||||
RedirectingGuard
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
@ -966,6 +978,28 @@ describe('Integration', () => {
|
||||
|
||||
expect(location.path()).toEqual('/simple');
|
||||
}));
|
||||
|
||||
it('should skip duplicate location events', fakeAsync(() => {
|
||||
const router = TestBed.inject(Router);
|
||||
const location = TestBed.inject(Location) as unknown as SpyLocation;
|
||||
const fixture = createRoot(router, RootCmp);
|
||||
|
||||
router.resetConfig([
|
||||
{path: 'blocked', component: SimpleCmp, canActivate: [RedirectingGuard]},
|
||||
{path: 'simple', component: SimpleCmp}
|
||||
]);
|
||||
router.navigateByUrl('/simple');
|
||||
advance(fixture);
|
||||
|
||||
const recordedEvents = [] as Event[];
|
||||
router.events.forEach(e => onlyNavigationStartAndEnd(e) && recordedEvents.push(e));
|
||||
|
||||
location.simulateUrlPop('/blocked');
|
||||
location.simulateHashChange('/blocked');
|
||||
|
||||
advance(fixture);
|
||||
expectEvents(recordedEvents, [[NavigationStart, '/blocked']]);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should support secondary routes', fakeAsync(inject([Router], (router: Router) => {
|
||||
@ -3878,8 +3912,8 @@ describe('Integration', () => {
|
||||
|
||||
expectEvents(recordedEvents, [
|
||||
[NavigationStart, '/lazyFalse/loaded'],
|
||||
// No GuardCheck events as `canLoad` is a special guard that's not actually part of the
|
||||
// guard lifecycle.
|
||||
// No GuardCheck events as `canLoad` is a special guard that's not actually part of
|
||||
// the guard lifecycle.
|
||||
[NavigationCancel, '/lazyFalse/loaded'],
|
||||
|
||||
[NavigationStart, '/blank'], [RoutesRecognized, '/blank'],
|
||||
@ -4842,8 +4876,8 @@ describe('Integration', () => {
|
||||
constructor(
|
||||
lazy: LazyParentComponent, // should be able to inject lazy/direct parent
|
||||
lazyService: LazyLoadedServiceDefinedInModule, // should be able to inject lazy service
|
||||
eager:
|
||||
EagerParentComponent // should use the injector of the location to create a parent
|
||||
eager: EagerParentComponent // should use the injector of the location to create a
|
||||
// parent
|
||||
) {}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user