diff --git a/packages/core/test/test_bed_spec.ts b/packages/core/test/test_bed_spec.ts index 041c5e7be1..160cd24b28 100644 --- a/packages/core/test/test_bed_spec.ts +++ b/packages/core/test/test_bed_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Compiler, Component, Directive, ErrorHandler, Inject, Injectable, InjectionToken, Injector, Input, ModuleWithProviders, NgModule, Optional, Pipe, ViewChild, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵdefineNgModule as defineNgModule, ɵɵtext as text} from '@angular/core'; +import {Compiler, Component, Directive, ErrorHandler, Inject, Injectable, InjectionToken, Injector, Input, ModuleWithProviders, NgModule, Optional, Pipe, Type, ViewChild, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵdefineNgModule as defineNgModule, ɵɵtext as text} from '@angular/core'; import {TestBed, getTestBed} from '@angular/core/testing/src/test_bed'; import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -715,6 +715,36 @@ describe('TestBed', () => { expect(fixture.nativeElement.innerHTML).toEqual('Inner'); }); + onlyInIvy('Ivy-specific errors').describe('checking types before compiling them', () => { + @Directive({ + selector: 'my-dir', + }) + class MyDir { + } + + @NgModule() + class MyModule { + } + + // [decorator, type, overrideFn] + const cases: [string, Type, string][] = [ + ['Component', MyDir, 'overrideComponent'], + ['NgModule', MyDir, 'overrideModule'], + ['Pipe', MyModule, 'overridePipe'], + ['Directive', MyModule, 'overrideDirective'], + ]; + cases.forEach(([decorator, type, overrideFn]) => { + it(`should throw an error in case invalid type is used in ${overrideFn} function`, () => { + TestBed.configureTestingModule({declarations: [MyDir]}); + expect(() => { + (TestBed as any)[overrideFn](type, {}); + TestBed.createComponent(type); + }).toThrowError(new RegExp(`class doesn't have @${decorator} decorator`, 'g')); + }); + }); + }); + + onlyInIvy('TestBed should handle AOT pre-compiled Components') .describe('AOT pre-compiled components', () => { /** diff --git a/packages/core/testing/src/r3_test_bed_compiler.ts b/packages/core/testing/src/r3_test_bed_compiler.ts index 9f17920c52..5c35e420ef 100644 --- a/packages/core/testing/src/r3_test_bed_compiler.ts +++ b/packages/core/testing/src/r3_test_bed_compiler.ts @@ -129,10 +129,10 @@ export class R3TestBedCompiler { this.resolvers.module.addOverride(ngModule, override); const metadata = this.resolvers.module.resolve(ngModule); if (metadata === null) { - throw new Error(`${ngModule.name} is not an @NgModule or is missing metadata`); + throw invalidTypeError(ngModule.name, 'NgModule'); } - this.recompileNgModule(ngModule); + this.recompileNgModule(ngModule, metadata); // At this point, the module has a valid module def (ɵmod), but the override may have introduced // new declarations or imported modules. Ingest any possible new types and add them to the @@ -306,7 +306,10 @@ export class R3TestBedCompiler { let needsAsyncResources = false; this.pendingComponents.forEach(declaration => { needsAsyncResources = needsAsyncResources || isComponentDefPendingResolution(declaration); - const metadata = this.resolvers.component.resolve(declaration) !; + const metadata = this.resolvers.component.resolve(declaration); + if (metadata === null) { + throw invalidTypeError(declaration.name, 'Component'); + } this.maybeStoreNgDef(NG_COMP_DEF, declaration); compileComponent(declaration, metadata); }); @@ -314,13 +317,19 @@ export class R3TestBedCompiler { this.pendingDirectives.forEach(declaration => { const metadata = this.resolvers.directive.resolve(declaration); + if (metadata === null) { + throw invalidTypeError(declaration.name, 'Directive'); + } this.maybeStoreNgDef(NG_DIR_DEF, declaration); compileDirective(declaration, metadata); }); this.pendingDirectives.clear(); this.pendingPipes.forEach(declaration => { - const metadata = this.resolvers.pipe.resolve(declaration) !; + const metadata = this.resolvers.pipe.resolve(declaration); + if (metadata === null) { + throw invalidTypeError(declaration.name, 'Pipe'); + } this.maybeStoreNgDef(NG_PIPE_DEF, declaration); compilePipe(declaration, metadata); }); @@ -434,11 +443,7 @@ export class R3TestBedCompiler { } } - private recompileNgModule(ngModule: Type): void { - const metadata = this.resolvers.module.resolve(ngModule); - if (metadata === null) { - throw new Error(`Unable to resolve metadata for NgModule: ${ngModule.name}`); - } + private recompileNgModule(ngModule: Type, metadata: NgModule): void { // Cache the initial ngModuleDef as it will be overwritten. this.maybeStoreNgDef(NG_MOD_DEF, ngModule); this.maybeStoreNgDef(NG_INJ_DEF, ngModule); @@ -751,6 +756,10 @@ function forEachRight(values: T[], fn: (value: T, idx: number) => void): void } } +function invalidTypeError(name: string, expectedType: string): Error { + return new Error(`${name} class doesn't have @${expectedType} decorator or is missing metadata.`); +} + class R3TestCompiler implements Compiler { constructor(private testBed: R3TestBedCompiler) {}