diff --git a/packages/router/test/bootstrap.spec.ts b/packages/router/test/bootstrap.spec.ts index 6127ef76d40..51c6b2a4ac9 100644 --- a/packages/router/test/bootstrap.spec.ts +++ b/packages/router/test/bootstrap.spec.ts @@ -6,9 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {APP_BASE_HREF, DOCUMENT, HashLocationStrategy, LocationStrategy, ɵgetDOM as getDOM} from '@angular/common'; -import {ApplicationRef, Component, CUSTOM_ELEMENTS_SCHEMA, destroyPlatform, Injectable, NgModule} from '@angular/core'; -import {inject} from '@angular/core/testing'; +import {DOCUMENT, PlatformLocation, ɵgetDOM as getDOM} from '@angular/common'; +import {BrowserPlatformLocation} from '@angular/common/src/location/platform_location'; +import {MockPlatformLocation} from '@angular/common/testing'; +import {ApplicationRef, Component, CUSTOM_ELEMENTS_SCHEMA, destroyPlatform, ENVIRONMENT_INITIALIZER, inject, Injectable, NgModule} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {NavigationEnd, provideRouter, Resolve, Router, RouterModule, RouterOutlet, withEnabledBlockingInitialNavigation} from '@angular/router'; @@ -58,33 +60,39 @@ describe('bootstrap', () => { } } - beforeEach(inject([DOCUMENT], (doc: any) => { + let navigationEndPromise: Promise; + beforeEach(() => { destroyPlatform(); + const doc = TestBed.inject(DOCUMENT); + const oldRoots = doc.querySelectorAll('test-app,test-app2'); + for (let i = 0; i < oldRoots.length; i++) { + getDOM().remove(oldRoots[i]); + } const el1 = getDOM().createElement('test-app', doc); const el2 = getDOM().createElement('test-app2', doc); doc.body.appendChild(el1); doc.body.appendChild(el2); + const {promise, resolveFn} = createPromise(); + navigationEndPromise = promise; log = []; - testProviders = [{provide: APP_BASE_HREF, useValue: ''}]; - })); + testProviders = [ + {provide: PlatformLocation, useClass: MockPlatformLocation}, + provideNavigationEndAction(resolveFn) + ]; + }); - afterEach(inject([DOCUMENT], (doc: any) => { - const oldRoots = doc.querySelectorAll('test-app,test-app2'); - for (let i = 0; i < oldRoots.length; i++) { - getDOM().remove(oldRoots[i]); - } - })); + afterEach(destroyPlatform); - it('should complete resolvers when initial navigation fails and initialNavigation = enabledBlocking', + it('should complete when initial navigation fails and initialNavigation = enabledBlocking', async () => { @NgModule({ imports: [BrowserModule], declarations: [RootCmp], bootstrap: [RootCmp], providers: [ - ...testProviders, {provide: LocationStrategy, useClass: HashLocationStrategy}, + ...testProviders, provideRouter( [{ matcher: () => { @@ -130,31 +138,18 @@ describe('bootstrap', () => { class BlankCmp { } - let resolveFn: () => void; - const navigationEndPromise = new Promise(r => { - resolveFn = r; - }); - @NgModule({ imports: [BrowserModule, RouterOutlet], declarations: [RootCmp], bootstrap: [RootCmp], providers: [ - {provide: LocationStrategy, useClass: HashLocationStrategy}, + ...testProviders, provideRouter( [{path: '', component: Renavigate}, {path: 'other', component: BlankCmp}], withEnabledBlockingInitialNavigation()) ], }) class TestModule { - constructor(router: Router) { - router.events.subscribe(e => { - if (e instanceof NavigationEnd) { - resolveFn(); - expect(router.url).toEqual('/other'); - } - }); - } } await Promise.all( @@ -178,7 +173,7 @@ describe('bootstrap', () => { {path: 'redirectToMe', children: [], resolve: {test: TestResolver}}, {path: '**', canActivate: [Redirect], children: []} ], - {useHash: true, initialNavigation: 'enabledBlocking'}) + {initialNavigation: 'enabledBlocking'}) ], declarations: [RootCmp], bootstrap: [RootCmp], @@ -186,36 +181,34 @@ describe('bootstrap', () => { schemas: [CUSTOM_ELEMENTS_SCHEMA] }) class TestModule { - constructor(router: Router) { + constructor() { log.push('TestModule'); } } - await platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { - const router = res.injector.get(Router); + const bootstrapPromise = platformBrowserDynamic([]).bootstrapModule(TestModule).then((ref) => { + const router = ref.injector.get(Router); expect(router.navigated).toEqual(true); expect(router.url).toContain('redirectToMe'); expect(log).toContain('TestModule'); }); + + await Promise.all([bootstrapPromise, navigationEndPromise]); }); it('should wait for redirect with UrlTree when initialNavigation = enabledBlocking', async () => { - @Injectable({providedIn: 'root'}) - class Redirect { - constructor(private router: Router) {} - canActivate() { - return this.router.createUrlTree(['/redirectToMe']); - } - } @NgModule({ imports: [ BrowserModule, RouterModule.forRoot( [ - {path: 'redirectToMe', children: [], resolve: {test: TestResolver}}, - {path: '**', canActivate: [Redirect], children: []} + {path: 'redirectToMe', children: []}, { + path: '**', + canActivate: [() => inject(Router).createUrlTree(['redirectToMe'])], + children: [] + } ], - {useHash: true, initialNavigation: 'enabledBlocking'}) + {initialNavigation: 'enabledBlocking'}) ], declarations: [RootCmp], bootstrap: [RootCmp], @@ -223,20 +216,22 @@ describe('bootstrap', () => { schemas: [CUSTOM_ELEMENTS_SCHEMA] }) class TestModule { - constructor(router: Router) { + constructor() { log.push('TestModule'); } } - await platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { - const router = res.injector.get(Router); + const bootstrapPromise = platformBrowserDynamic([]).bootstrapModule(TestModule).then((ref) => { + const router = ref.injector.get(Router); expect(router.navigated).toEqual(true); expect(router.url).toContain('redirectToMe'); expect(log).toContain('TestModule'); }); + + await Promise.all([bootstrapPromise, navigationEndPromise]); }); - it('should wait for resolvers to complete when initialNavigation = enabledBlocking', (done) => { + it('should wait for resolvers to complete when initialNavigation = enabledBlocking', async () => { @Component({selector: 'test', template: 'test'}) class TestCmpEnabled { } @@ -246,7 +241,7 @@ describe('bootstrap', () => { BrowserModule, RouterModule.forRoot( [{path: '**', component: TestCmpEnabled, resolve: {test: TestResolver}}], - {useHash: true, initialNavigation: 'enabledBlocking'}) + {initialNavigation: 'enabledBlocking'}) ], declarations: [RootCmp, TestCmpEnabled], bootstrap: [RootCmp], @@ -257,16 +252,16 @@ describe('bootstrap', () => { constructor(router: Router) {} } - platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { - const router = res.injector.get(Router); + const bootstrapPromise = platformBrowserDynamic([]).bootstrapModule(TestModule).then((ref) => { + const router = ref.injector.get(Router); const data = router.routerState.snapshot.root.firstChild!.data; expect(data['test']).toEqual('test-data'); - done(); }); + await Promise.all([bootstrapPromise, navigationEndPromise]); }); it('should NOT wait for resolvers to complete when initialNavigation = enabledNonBlocking', - (done) => { + async () => { @Component({selector: 'test', template: 'test'}) class TestCmpLegacyEnabled { } @@ -276,7 +271,7 @@ describe('bootstrap', () => { BrowserModule, RouterModule.forRoot( [{path: '**', component: TestCmpLegacyEnabled, resolve: {test: TestResolver}}], - {useHash: true, initialNavigation: 'enabledNonBlocking'}) + {initialNavigation: 'enabledNonBlocking'}) ], declarations: [RootCmp, TestCmpLegacyEnabled], bootstrap: [RootCmp], @@ -290,24 +285,21 @@ describe('bootstrap', () => { } } - platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { - const router: Router = res.injector.get(Router); - expect(router.routerState.snapshot.root.firstChild).toBeNull(); - // ResolveEnd has not been emitted yet because bootstrap returned too early - expect(log).toEqual([ - 'TestModule', 'RootCmp', 'NavigationStart', 'RoutesRecognized', 'GuardsCheckStart', - 'ChildActivationStart', 'ActivationStart', 'GuardsCheckEnd', 'ResolveStart' - ]); + const bootstrapPromise = + platformBrowserDynamic([]).bootstrapModule(TestModule).then((ref) => { + const router: Router = ref.injector.get(Router); + expect(router.routerState.snapshot.root.firstChild).toBeNull(); + // ResolveEnd has not been emitted yet because bootstrap returned too early + expect(log).toEqual([ + 'TestModule', 'RootCmp', 'NavigationStart', 'RoutesRecognized', 'GuardsCheckStart', + 'ChildActivationStart', 'ActivationStart', 'GuardsCheckEnd', 'ResolveStart' + ]); + }); - router.events.subscribe((e) => { - if (e instanceof NavigationEnd) { - done(); - } - }); - }); + await Promise.all([bootstrapPromise, navigationEndPromise]); }); - it('should NOT wait for resolvers to complete when initialNavigation is not set', (done) => { + it('should NOT wait for resolvers to complete when initialNavigation is not set', async () => { @Component({selector: 'test', template: 'test'}) class TestCmpLegacyEnabled { } @@ -317,7 +309,7 @@ describe('bootstrap', () => { BrowserModule, RouterModule.forRoot( [{path: '**', component: TestCmpLegacyEnabled, resolve: {test: TestResolver}}], - {useHash: true}) + ) ], declarations: [RootCmp, TestCmpLegacyEnabled], bootstrap: [RootCmp], @@ -331,21 +323,17 @@ describe('bootstrap', () => { } } - platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { - const router: Router = res.injector.get(Router); + const bootstrapPromise = platformBrowserDynamic([]).bootstrapModule(TestModule).then(ref => { + const router: Router = ref.injector.get(Router); expect(router.routerState.snapshot.root.firstChild).toBeNull(); // ResolveEnd has not been emitted yet because bootstrap returned too early expect(log).toEqual([ 'TestModule', 'RootCmp', 'NavigationStart', 'RoutesRecognized', 'GuardsCheckStart', 'ChildActivationStart', 'ActivationStart', 'GuardsCheckEnd', 'ResolveStart' ]); - - router.events.subscribe((e) => { - if (e instanceof NavigationEnd) { - done(); - } - }); }); + + await Promise.all([bootstrapPromise, navigationEndPromise]); }); it('should not run navigation when initialNavigation = disabled', (done) => { @@ -358,7 +346,7 @@ describe('bootstrap', () => { BrowserModule, RouterModule.forRoot( [{path: '**', component: TestCmpDiabled, resolve: {test: TestResolver}}], - {useHash: true, initialNavigation: 'disabled'}) + {initialNavigation: 'disabled'}) ], declarations: [RootCmp, TestCmpDiabled], bootstrap: [RootCmp], @@ -380,9 +368,14 @@ describe('bootstrap', () => { }); it('should not init router navigation listeners if a non root component is bootstrapped', - (done) => { + async () => { @NgModule({ - imports: [BrowserModule, RouterModule.forRoot([], {useHash: true})], + imports: [ + BrowserModule, + RouterModule.forRoot( + [], + ) + ], declarations: [SecondRootCmp, RootCmp], bootstrap: [RootCmp], providers: testProviders, @@ -391,7 +384,7 @@ describe('bootstrap', () => { class TestModule { } - platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { + await platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { const router = res.injector.get(Router); spyOn(router as any, 'resetRootComponentType').and.callThrough(); @@ -399,15 +392,13 @@ describe('bootstrap', () => { appRef.bootstrap(SecondRootCmp); expect((router as any).resetRootComponentType).not.toHaveBeenCalled(); - - done(); }); }); it('should reinit router navigation listeners if a previously bootstrapped root component is destroyed', - (done) => { + async () => { @NgModule({ - imports: [BrowserModule, RouterModule.forRoot([], {useHash: true})], + imports: [BrowserModule, RouterModule.forRoot([])], declarations: [SecondRootCmp, RootCmp], bootstrap: [RootCmp], providers: testProviders, @@ -416,18 +407,20 @@ describe('bootstrap', () => { class TestModule { } - platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { + await platformBrowserDynamic([]).bootstrapModule(TestModule).then(res => { const router = res.injector.get(Router); spyOn(router as any, 'resetRootComponentType').and.callThrough(); const appRef: ApplicationRef = res.injector.get(ApplicationRef); + const {promise, resolveFn} = createPromise(); appRef.components[0].onDestroy(() => { appRef.bootstrap(SecondRootCmp); expect((router as any).resetRootComponentType).toHaveBeenCalled(); - done(); + resolveFn(); }); appRef.components[0].destroy(); + return promise; }); }); @@ -455,10 +448,9 @@ describe('bootstrap', () => { {path: '', pathMatch: 'full', redirectTo: '/aa'}, {path: 'aa', component: TallComponent}, {path: 'bb', component: TallComponent}, {path: 'cc', component: TallComponent}, - {path: 'fail', component: TallComponent, canActivate: ['returnFalse']} + {path: 'fail', component: TallComponent, canActivate: [() => false]} ], { - useHash: true, scrollPositionRestoration: 'enabled', anchorScrolling: 'enabled', scrollOffset: [0, 100], @@ -467,7 +459,7 @@ describe('bootstrap', () => { ], declarations: [TallComponent, RootCmp], bootstrap: [RootCmp], - providers: [...testProviders, {provide: 'returnFalse', useValue: () => false}], + providers: [...testProviders], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) class TestModule { @@ -515,7 +507,7 @@ describe('bootstrap', () => { imports: [BrowserModule, RouterModule.forRoot([])], declarations: [RootCmp], bootstrap: [RootCmp], - providers: testProviders, + providers: [...testProviders, {provide: PlatformLocation, useClass: BrowserPlatformLocation}], }) class TestModule { } @@ -568,3 +560,29 @@ describe('bootstrap', () => { })(); }); }); + +function onNavigationEnd(router: Router, fn: Function) { + router.events.subscribe(e => { + if (e instanceof NavigationEnd) { + fn(); + } + }); +} + +function provideNavigationEndAction(fn: Function) { + return { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useValue: () => { + onNavigationEnd(inject(Router), fn); + } + }; +} + +function createPromise() { + let resolveFn: () => void; + const promise = new Promise(r => { + resolveFn = r; + }); + return {resolveFn: () => resolveFn(), promise}; +}