mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
50d7916278
With the deprecation of the configurable errorHandler in the Router, there is a missing use-case to prevent the navigation promise from rejecting on an error. This rejection results in unhandled promise rejections. This commit allows developers to instruct the router to instead resolve the navigation promise with 'false', which matches the behavior of other failed navigations. Resolving the Promise would be the ideal default behavior. It is rare that any code handles the navigation Promise at all and even more rare that the Promise rejection is caught. Updating the default value for this option should be considered for an upcoming major version. fixes #48902 PR Close #48910
565 lines
21 KiB
TypeScript
565 lines
21 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import {CommonModule, Location} from '@angular/common';
|
|
import {provideLocationMocks, SpyLocation} from '@angular/common/testing';
|
|
import {Component, Injectable, NgModule, Type} from '@angular/core';
|
|
import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
|
|
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
|
import {Router, RouterModule, RouterOutlet, UrlTree, withRouterConfig} from '@angular/router';
|
|
import {EMPTY, of} from 'rxjs';
|
|
|
|
import {provideRouter} from '../src/provide_router';
|
|
import {isUrlTree} from '../src/url_tree';
|
|
|
|
describe('`restoredState#ɵrouterPageId`', () => {
|
|
@Injectable({providedIn: 'root'})
|
|
class MyCanDeactivateGuard {
|
|
allow: boolean = true;
|
|
canDeactivate(): boolean {
|
|
return this.allow;
|
|
}
|
|
}
|
|
|
|
@Injectable({providedIn: 'root'})
|
|
class ThrowingCanActivateGuard {
|
|
throw = false;
|
|
|
|
constructor(private router: Router) {}
|
|
|
|
canActivate(): boolean {
|
|
if (this.throw) {
|
|
throw new Error('error in guard');
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
@Injectable({providedIn: 'root'})
|
|
class MyCanActivateGuard {
|
|
allow: boolean = true;
|
|
redirectTo: string|null|UrlTree = null;
|
|
|
|
constructor(private router: Router) {}
|
|
|
|
canActivate(): boolean|UrlTree {
|
|
if (typeof this.redirectTo === 'string') {
|
|
this.router.navigateByUrl(this.redirectTo);
|
|
} else if (isUrlTree(this.redirectTo)) {
|
|
return this.redirectTo;
|
|
}
|
|
return this.allow;
|
|
}
|
|
}
|
|
@Injectable({providedIn: 'root'})
|
|
class MyResolve {
|
|
myresolve = of(2);
|
|
resolve() {
|
|
return this.myresolve;
|
|
}
|
|
}
|
|
|
|
let fixture: ComponentFixture<unknown>;
|
|
|
|
function createNavigationHistory(urlUpdateStrategy: 'eager'|'deferred' = 'deferred') {
|
|
TestBed.configureTestingModule({
|
|
imports: [TestModule],
|
|
providers: [
|
|
{provide: 'alwaysFalse', useValue: (a: any) => false},
|
|
{provide: Location, useClass: SpyLocation},
|
|
provideRouter(
|
|
[
|
|
{
|
|
path: 'first',
|
|
component: SimpleCmp,
|
|
canDeactivate: [MyCanDeactivateGuard],
|
|
canActivate: [MyCanActivateGuard, ThrowingCanActivateGuard],
|
|
resolve: {x: MyResolve}
|
|
},
|
|
{
|
|
path: 'second',
|
|
component: SimpleCmp,
|
|
canDeactivate: [MyCanDeactivateGuard],
|
|
canActivate: [MyCanActivateGuard, ThrowingCanActivateGuard],
|
|
resolve: {x: MyResolve}
|
|
},
|
|
{
|
|
path: 'third',
|
|
component: SimpleCmp,
|
|
canDeactivate: [MyCanDeactivateGuard],
|
|
canActivate: [MyCanActivateGuard, ThrowingCanActivateGuard],
|
|
resolve: {x: MyResolve}
|
|
},
|
|
{
|
|
path: 'unguarded',
|
|
component: SimpleCmp,
|
|
},
|
|
{
|
|
path: 'throwing',
|
|
component: ThrowingCmp,
|
|
},
|
|
{
|
|
path: 'loaded',
|
|
loadChildren: () => of(ModuleWithSimpleCmpAsRoute),
|
|
canLoad: ['alwaysFalse']
|
|
}
|
|
],
|
|
withRouterConfig({
|
|
urlUpdateStrategy,
|
|
canceledNavigationResolution: 'computed',
|
|
resolveNavigationPromiseOnError: true,
|
|
})),
|
|
]
|
|
});
|
|
const router = TestBed.inject(Router);
|
|
const location = TestBed.inject(Location);
|
|
fixture = createRoot(router, RootCmp);
|
|
router.initialNavigation();
|
|
advance(fixture);
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 0}));
|
|
|
|
router.navigateByUrl('/first');
|
|
advance(fixture);
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
|
|
|
router.navigateByUrl('/second');
|
|
advance(fixture);
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
router.navigateByUrl('/third');
|
|
advance(fixture);
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
|
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
}
|
|
|
|
describe('deferred url updates', () => {
|
|
beforeEach(fakeAsync(() => {
|
|
createNavigationHistory();
|
|
}));
|
|
|
|
it('should work when CanActivate returns false', fakeAsync(() => {
|
|
const location = TestBed.inject(Location);
|
|
const router = TestBed.inject(Router);
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
TestBed.inject(MyCanActivateGuard).allow = false;
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
TestBed.inject(MyCanActivateGuard).allow = true;
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/first');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
|
|
|
TestBed.inject(MyCanActivateGuard).allow = false;
|
|
location.forward();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/first');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
|
|
|
router.navigateByUrl('/second');
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/first');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
|
}));
|
|
|
|
|
|
it('should work when CanDeactivate returns false', fakeAsync(() => {
|
|
const location = TestBed.inject(Location);
|
|
const router = TestBed.inject(Router);
|
|
|
|
TestBed.inject(MyCanDeactivateGuard).allow = false;
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
location.forward();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
router.navigateByUrl('third');
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
|
|
TestBed.inject(MyCanDeactivateGuard).allow = true;
|
|
location.forward();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/third');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
|
}));
|
|
|
|
it('should work when using `NavigationExtras.skipLocationChange`', fakeAsync(() => {
|
|
const location = TestBed.inject(Location);
|
|
const router = TestBed.inject(Router);
|
|
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
router.navigateByUrl('/first', {skipLocationChange: true});
|
|
advance(fixture);
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
router.navigateByUrl('/third');
|
|
advance(fixture);
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
|
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
}));
|
|
|
|
it('should work when using `NavigationExtras.replaceUrl`', fakeAsync(() => {
|
|
const location = TestBed.inject(Location);
|
|
const router = TestBed.inject(Router);
|
|
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
router.navigateByUrl('/first', {replaceUrl: true});
|
|
advance(fixture);
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
expect(location.path()).toEqual('/first');
|
|
}));
|
|
|
|
it('should work when CanLoad returns false', fakeAsync(() => {
|
|
const location = TestBed.inject(Location);
|
|
const router = TestBed.inject(Router);
|
|
|
|
router.navigateByUrl('/loaded');
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
}));
|
|
|
|
it('should work when resolve empty', fakeAsync(() => {
|
|
const location = TestBed.inject(Location);
|
|
const router = TestBed.inject(Router);
|
|
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
TestBed.inject(MyResolve).myresolve = EMPTY;
|
|
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
expect(location.path()).toEqual('/second');
|
|
|
|
TestBed.inject(MyResolve).myresolve = of(2);
|
|
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
|
expect(location.path()).toEqual('/first');
|
|
|
|
TestBed.inject(MyResolve).myresolve = EMPTY;
|
|
|
|
// We should cancel the navigation to `/third` when myresolve is empty
|
|
router.navigateByUrl('/third');
|
|
advance(fixture);
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
|
expect(location.path()).toEqual('/first');
|
|
|
|
location.historyGo(2);
|
|
advance(fixture);
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
|
expect(location.path()).toEqual('/first');
|
|
|
|
TestBed.inject(MyResolve).myresolve = of(2);
|
|
location.historyGo(2);
|
|
advance(fixture);
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
|
expect(location.path()).toEqual('/third');
|
|
|
|
TestBed.inject(MyResolve).myresolve = EMPTY;
|
|
location.historyGo(-2);
|
|
advance(fixture);
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
|
expect(location.path()).toEqual('/third');
|
|
}));
|
|
|
|
|
|
it('should work when an error occurred during navigation', fakeAsync(() => {
|
|
const location = TestBed.inject(Location);
|
|
const router = TestBed.inject(Router);
|
|
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
|
|
router.navigateByUrl('/invalid').catch(() => null);
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/first');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
|
}));
|
|
|
|
it('should work when CanActivate redirects', fakeAsync(() => {
|
|
const location = TestBed.inject(Location);
|
|
|
|
TestBed.inject(MyCanActivateGuard).redirectTo = '/unguarded';
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/unguarded');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
TestBed.inject(MyCanActivateGuard).redirectTo = null;
|
|
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/first');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
|
}));
|
|
|
|
it('restores history correctly when component throws error in constructor and replaceUrl=true',
|
|
fakeAsync(() => {
|
|
const location = TestBed.inject(Location);
|
|
const router = TestBed.inject(Router);
|
|
|
|
router.navigateByUrl('/throwing', {replaceUrl: true}).catch(() => null);
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/first');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
|
}));
|
|
|
|
it('restores history correctly when component throws error in constructor and skipLocationChange=true',
|
|
fakeAsync(() => {
|
|
const location = TestBed.inject(Location);
|
|
const router = TestBed.inject(Router);
|
|
|
|
router.navigateByUrl('/throwing', {skipLocationChange: true}).catch(() => null);
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/first');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
|
}));
|
|
});
|
|
|
|
describe('eager url updates', () => {
|
|
beforeEach(fakeAsync(() => {
|
|
createNavigationHistory('eager');
|
|
}));
|
|
it('should work', fakeAsync(() => {
|
|
const location = TestBed.inject(Location) as SpyLocation;
|
|
const router = TestBed.inject(Router);
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
TestBed.inject(MyCanActivateGuard).allow = false;
|
|
router.navigateByUrl('/first');
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
}));
|
|
it('should work when CanActivate redirects', fakeAsync(() => {
|
|
const location = TestBed.inject(Location);
|
|
const router = TestBed.inject(Router);
|
|
|
|
TestBed.inject(MyCanActivateGuard).redirectTo = '/unguarded';
|
|
router.navigateByUrl('/third');
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/unguarded');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4}));
|
|
|
|
TestBed.inject(MyCanActivateGuard).redirectTo = null;
|
|
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/third');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
|
}));
|
|
it('should work when CanActivate redirects with UrlTree', fakeAsync(() => {
|
|
// Note that this test is different from the above case because we are able to specifically
|
|
// handle the `UrlTree` case as a proper redirect and set `replaceUrl: true` on the
|
|
// follow-up navigation.
|
|
const location = TestBed.inject(Location);
|
|
const router = TestBed.inject(Router);
|
|
let allowNavigation = true;
|
|
router.resetConfig([
|
|
{path: 'initial', children: []},
|
|
{path: 'redirectFrom', redirectTo: 'redirectTo'},
|
|
{path: 'redirectTo', children: [], canActivate: [() => allowNavigation]},
|
|
]);
|
|
|
|
// already at '2' from the `beforeEach` navigations
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
router.navigateByUrl('/initial');
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/initial');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
|
|
|
TestBed.inject(MyCanActivateGuard).redirectTo = null;
|
|
|
|
router.navigateByUrl('redirectTo');
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/redirectTo');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4}));
|
|
|
|
// Navigate to different URL but get redirected to same URL should result in same page id
|
|
router.navigateByUrl('redirectFrom');
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/redirectTo');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4}));
|
|
|
|
// Back and forward should have page IDs 1 apart
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/initial');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
|
location.forward();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/redirectTo');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4}));
|
|
|
|
// Rejected navigation after redirect to same URL should have the same page ID
|
|
allowNavigation = false;
|
|
router.navigateByUrl('redirectFrom');
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/redirectTo');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 4}));
|
|
}));
|
|
it('redirectTo with same url, and guard reject', fakeAsync(() => {
|
|
const location = TestBed.inject(Location);
|
|
const router = TestBed.inject(Router);
|
|
|
|
TestBed.inject(MyCanActivateGuard).redirectTo = router.createUrlTree(['unguarded']);
|
|
router.navigateByUrl('/third');
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/unguarded');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 3}));
|
|
|
|
TestBed.inject(MyCanActivateGuard).redirectTo = null;
|
|
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
}));
|
|
});
|
|
|
|
|
|
for (const urlUpdateStrategy of ['deferred', 'eager'] as const) {
|
|
it(`restores history correctly when an error is thrown in guard with urlUpdateStrategy ${
|
|
urlUpdateStrategy}`,
|
|
fakeAsync(() => {
|
|
createNavigationHistory(urlUpdateStrategy);
|
|
const location = TestBed.inject(Location);
|
|
|
|
TestBed.inject(ThrowingCanActivateGuard).throw = true;
|
|
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
TestBed.inject(ThrowingCanActivateGuard).throw = false;
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/first');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
|
}));
|
|
|
|
it(`restores history correctly when component throws error in constructor with urlUpdateStrategy ${
|
|
urlUpdateStrategy}`,
|
|
fakeAsync(() => {
|
|
createNavigationHistory(urlUpdateStrategy);
|
|
const location = TestBed.inject(Location);
|
|
const router = TestBed.inject(Router);
|
|
|
|
router.navigateByUrl('/throwing').catch(() => null);
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/second');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 2}));
|
|
|
|
location.back();
|
|
advance(fixture);
|
|
expect(location.path()).toEqual('/first');
|
|
expect(location.getState()).toEqual(jasmine.objectContaining({ɵrouterPageId: 1}));
|
|
}));
|
|
}
|
|
});
|
|
|
|
function createRoot<T>(router: Router, type: Type<T>): ComponentFixture<T> {
|
|
const f = TestBed.createComponent(type);
|
|
advance(f);
|
|
router.initialNavigation();
|
|
advance(f);
|
|
return f;
|
|
}
|
|
|
|
@Component({selector: 'simple-cmp', template: `simple`})
|
|
class SimpleCmp {
|
|
}
|
|
|
|
@NgModule(
|
|
{imports: [RouterModule.forChild([{path: '', component: SimpleCmp}])]},
|
|
)
|
|
class ModuleWithSimpleCmpAsRoute {
|
|
}
|
|
|
|
@Component({selector: 'root-cmp', template: `<router-outlet></router-outlet>`})
|
|
class RootCmp {
|
|
}
|
|
|
|
@Component({selector: 'throwing-cmp', template: ''})
|
|
class ThrowingCmp {
|
|
constructor() {
|
|
throw new Error('Throwing Cmp');
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function advance(fixture: ComponentFixture<unknown>, millis?: number): void {
|
|
tick(millis);
|
|
fixture.detectChanges();
|
|
}
|
|
|
|
@NgModule({
|
|
imports: [
|
|
RouterOutlet,
|
|
CommonModule,
|
|
],
|
|
providers: [
|
|
provideLocationMocks(),
|
|
],
|
|
exports: [SimpleCmp, RootCmp, ThrowingCmp],
|
|
declarations: [SimpleCmp, RootCmp, ThrowingCmp]
|
|
})
|
|
class TestModule {
|
|
}
|