mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
test(router): Update flaky tests to consistently pass (#48176)
This commit makes several updates to the bootstrap tests of the Router. The change that made the real difference was providing `MockPlatformLocation`. Other changes include: * using `async/await` instead of `done` * not providing APP_BASE_HREF (recent router refactors made this unnecessary) * using some functional guards to simplify some tests PR Close #48176
This commit is contained in:
@@ -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<void>;
|
||||
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<void>(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<void>(r => {
|
||||
resolveFn = r;
|
||||
});
|
||||
return {resolveFn: () => resolveFn(), promise};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user