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:
Andrew Scott
2020-06-22 09:29:00 -07:00
parent 69472a1ed0
commit 9185c6e971
3 changed files with 118 additions and 45 deletions

View File

@ -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
) {}
}