angular-cn/packages/compiler/test/metadata_resolver_spec.ts
Joey Perrott d1ea1f4c7f build: update license headers to reference Google LLC ()
Update the license headers throughout the repository to reference Google LLC
rather than Google Inc, for the required license headers.

PR Close 
2020-05-26 14:26:58 -04:00

554 lines
23 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 {LIFECYCLE_HOOKS_VALUES, LifecycleHooks} from '@angular/compiler/src/lifecycle_reflector';
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, Component, Directive, DoCheck, Injectable, NgModule, OnChanges, OnDestroy, OnInit, Pipe, SimpleChanges, ViewEncapsulation, ɵstringify as stringify} from '@angular/core';
import {async, inject, TestBed} from '@angular/core/testing';
import {CompileDiDependencyMetadata, identifierName} from '../src/compile_metadata';
import {CompileMetadataResolver} from '../src/metadata_resolver';
import {ResourceLoader} from '../src/resource_loader';
import {MockResourceLoader} from '../testing/src/resource_loader_mock';
import {MalformedStylesComponent} from './metadata_resolver_fixture';
import {TEST_COMPILER_PROVIDERS} from './test_bindings';
{
describe('CompileMetadataResolver', () => {
beforeEach(() => {
TestBed.configureCompiler({providers: TEST_COMPILER_PROVIDERS});
});
it('should throw on the getDirectiveMetadata/getPipeMetadata methods if the module has not been loaded yet',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({})
class SomeModule {
}
@Pipe({name: 'pipe'})
class SomePipe {
}
expect(() => resolver.getDirectiveMetadata(ComponentWithEverythingInline))
.toThrowError(/Illegal state/);
expect(() => resolver.getPipeMetadata(SomePipe)).toThrowError(/Illegal state/);
}));
it('should read metadata in sync for components with inline resources',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [ComponentWithEverythingInline]})
class SomeModule {
}
resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true);
const meta = resolver.getDirectiveMetadata(ComponentWithEverythingInline);
expect(meta.selector).toEqual('someSelector');
expect(meta.exportAs).toEqual('someExportAs');
expect(meta.isComponent).toBe(true);
expect(meta.type.reference).toBe(ComponentWithEverythingInline);
expect(identifierName(meta.type)).toEqual(stringify(ComponentWithEverythingInline));
expect(meta.type.lifecycleHooks).toEqual(LIFECYCLE_HOOKS_VALUES);
expect(meta.changeDetection).toBe(ChangeDetectionStrategy.Default);
expect(meta.inputs).toEqual({'someProp': 'someProp'});
expect(meta.outputs).toEqual({'someEvent': 'someEvent'});
expect(meta.hostListeners).toEqual({'someHostListener': 'someHostListenerExpr'});
expect(meta.hostProperties).toEqual({'someHostProp': 'someHostPropExpr'});
expect(meta.hostAttributes).toEqual({'someHostAttr': 'someHostAttrValue'});
expect(meta.template !.encapsulation).toBe(ViewEncapsulation.Emulated);
expect(meta.template !.styles).toEqual(['someStyle']);
expect(meta.template !.template).toEqual('someTemplate');
expect(meta.template !.interpolation).toEqual(['{{', '}}']);
}));
it('should throw when reading metadata for component with external resources when sync=true is passed',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [ComponentWithExternalResources]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(`Can't compile synchronously as ${
stringify(ComponentWithExternalResources)} is still being loaded!`);
}));
it('should read external metadata when sync=false',
async(inject(
[CompileMetadataResolver, ResourceLoader],
(resolver: CompileMetadataResolver, resourceLoader: MockResourceLoader) => {
@NgModule({declarations: [ComponentWithExternalResources]})
class SomeModule {
}
resourceLoader.when('someTemplateUrl', 'someTemplate');
resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, false).then(() => {
const meta = resolver.getDirectiveMetadata(ComponentWithExternalResources);
expect(meta.selector).toEqual('someSelector');
expect(meta.template !.styleUrls).toEqual(['someStyleUrl']);
expect(meta.template !.templateUrl).toEqual('someTemplateUrl');
expect(meta.template !.template).toEqual('someTemplate');
});
resourceLoader.flush();
})));
it('should use `./` as base url for templates during runtime compilation if no moduleId is given',
async(inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@Component({selector: 'someComponent', templateUrl: 'someUrl'})
class ComponentWithoutModuleId {
}
@NgModule({declarations: [ComponentWithoutModuleId]})
class SomeModule {
}
resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, false).then(() => {
const value: string =
resolver.getDirectiveMetadata(ComponentWithoutModuleId).template !.templateUrl!;
const expectedEndValue = './someUrl';
expect(value.endsWith(expectedEndValue)).toBe(true);
});
})));
it('should throw when the moduleId is not a string',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [ComponentWithInvalidModuleId]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(
`moduleId should be a string in "ComponentWithInvalidModuleId". See` +
` https://goo.gl/wIDDiL for more information.\n` +
`If you're using Webpack you should inline the template and the styles, see` +
` https://goo.gl/X2J8zc.`);
}));
it('should throw when metadata is incorrectly typed',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [MalformedStylesComponent]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(`Expected 'styles' to be an array of strings.`);
}));
it('should throw with descriptive error message when a module imports itself',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({imports: [SomeModule]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(`'SomeModule' module can't import itself`);
}));
it('should throw with descriptive error message when provider token can not be resolved',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [MyBrokenComp1]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(`Can't resolve all parameters for MyBrokenComp1: (?).`);
}));
it('should throw with descriptive error message when a directive is passed to imports',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({imports: [ComponentWithoutModuleId]})
class ModuleWithImportedComponent {
}
expect(
() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithImportedComponent, true))
.toThrowError(
`Unexpected directive 'ComponentWithoutModuleId' imported by the module 'ModuleWithImportedComponent'. Please add a @NgModule annotation.`);
}));
it('should throw with descriptive error message when a pipe is passed to imports',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@Pipe({name: 'somePipe'})
class SomePipe {
}
@NgModule({imports: [SomePipe]})
class ModuleWithImportedPipe {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithImportedPipe, true))
.toThrowError(
`Unexpected pipe 'SomePipe' imported by the module 'ModuleWithImportedPipe'. Please add a @NgModule annotation.`);
}));
it('should throw with descriptive error message when a module is passed to declarations',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({})
class SomeModule {
}
@NgModule({declarations: [SomeModule]})
class ModuleWithDeclaredModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithDeclaredModule, true))
.toThrowError(
`Unexpected module 'SomeModule' declared by the module 'ModuleWithDeclaredModule'. Please add a @Pipe/@Directive/@Component annotation.`);
}));
it('should throw with descriptive error message when a declared pipe is missing annotation',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
class SomePipe {}
@NgModule({declarations: [SomePipe]})
class ModuleWithDeclaredModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithDeclaredModule, true))
.toThrowError(
`Unexpected value 'SomePipe' declared by the module 'ModuleWithDeclaredModule'. Please add a @Pipe/@Directive/@Component annotation.`);
}));
it('should throw with descriptive error message when an imported module is missing annotation',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
class SomeModule {}
@NgModule({imports: [SomeModule]})
class ModuleWithImportedModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithImportedModule, true))
.toThrowError(
`Unexpected value 'SomeModule' imported by the module 'ModuleWithImportedModule'. Please add a @NgModule annotation.`);
}));
it('should throw with descriptive error message when null is passed to declarations',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [null!]})
class ModuleWithNullDeclared {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithNullDeclared, true))
.toThrowError(
`Unexpected value 'null' declared by the module 'ModuleWithNullDeclared'`);
}));
it('should throw with descriptive error message when null is passed to imports',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({imports: [null!]})
class ModuleWithNullImported {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithNullImported, true))
.toThrowError(
`Unexpected value 'null' imported by the module 'ModuleWithNullImported'`);
}));
it('should throw with descriptive error message when a param token of a dependency is undefined',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [MyBrokenComp2]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(`Can't resolve all parameters for NonAnnotatedService: (?).`);
}));
it('should throw with descriptive error message when encounter invalid provider',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({providers: [{provide: SimpleService, useClass: undefined!}]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(/Invalid provider for SimpleService. useClass cannot be undefined./);
}));
it('should throw with descriptive error message when provider is undefined',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({providers: [undefined!]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(/Encountered undefined provider!/);
}));
it('should throw with descriptive error message when one of providers is not present',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [MyBrokenComp3]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(
`Invalid providers for "MyBrokenComp3" - only instances of Provider and Type are allowed, got: [SimpleService, ?null?, ...]`);
}));
it('should throw with descriptive error message when one of viewProviders is not present',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [MyBrokenComp4]})
class SomeModule {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(SomeModule, true))
.toThrowError(
`Invalid viewProviders for "MyBrokenComp4" - only instances of Provider and Type are allowed, got: [?null?, ...]`);
}));
it('should throw with descriptive error message when null or undefined is passed to module bootstrap',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({bootstrap: [null!]})
class ModuleWithNullBootstrap {
}
@NgModule({bootstrap: [undefined!]})
class ModuleWithUndefinedBootstrap {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithNullBootstrap, true))
.toThrowError(
`Unexpected value 'null' used in the bootstrap property of module 'ModuleWithNullBootstrap'`);
expect(
() =>
resolver.loadNgModuleDirectiveAndPipeMetadata(ModuleWithUndefinedBootstrap, true))
.toThrowError(
`Unexpected value 'undefined' used in the bootstrap property of module 'ModuleWithUndefinedBootstrap'`);
}));
it('should throw an error when the interpolation config has invalid symbols',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [ComponentWithInvalidInterpolation1]})
class Module1 {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(Module1, true))
.toThrowError(`[' ', ' '] contains unusable interpolation symbol.`);
@NgModule({declarations: [ComponentWithInvalidInterpolation2]})
class Module2 {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(Module2, true))
.toThrowError(`['{', '}'] contains unusable interpolation symbol.`);
@NgModule({declarations: [ComponentWithInvalidInterpolation3]})
class Module3 {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(Module3, true))
.toThrowError(`['<%', '%>'] contains unusable interpolation symbol.`);
@NgModule({declarations: [ComponentWithInvalidInterpolation4]})
class Module4 {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(Module4, true))
.toThrowError(`['&#', '}}'] contains unusable interpolation symbol.`);
@NgModule({declarations: [ComponentWithInvalidInterpolation5]})
class Module5 {
}
expect(() => resolver.loadNgModuleDirectiveAndPipeMetadata(Module5, true))
.toThrowError(`['&lbrace;', '}}'] contains unusable interpolation symbol.`);
}));
it(`should throw an error when a Pipe is added to module's bootstrap list`,
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@Pipe({name: 'pipe'})
class MyPipe {
}
@NgModule({declarations: [MyPipe], bootstrap: [MyPipe]})
class ModuleWithPipeInBootstrap {
}
expect(() => resolver.getNgModuleMetadata(ModuleWithPipeInBootstrap))
.toThrowError(`MyPipe cannot be used as an entry component.`);
}));
it(`should throw an error when a Service is added to module's bootstrap list`,
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({declarations: [], bootstrap: [SimpleService]})
class ModuleWithServiceInBootstrap {
}
expect(() => resolver.getNgModuleMetadata(ModuleWithServiceInBootstrap))
.toThrowError(`SimpleService cannot be used as an entry component.`);
}));
it('should generate an error when a dependency could not be resolved',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
// Override the errorCollector so that error gets collected instead of
// being thrown.
(resolver as any)._errorCollector = (error: Error, type?: any) => {
expect(error.message).toBe(`Can't resolve all parameters for MyComponent: (?).`);
};
@Component({template: ''})
class MyComponent {
// @ts-ignore UserService is a non-existent class.
constructor(service: UserService) {}
}
@NgModule({declarations: [MyComponent]})
class AppModule {
}
const moduleMetadata = resolver.getNgModuleMetadata(AppModule)!;
expect(moduleMetadata).toBeTruthy();
expect(moduleMetadata.declaredDirectives.length).toBe(1);
const directive = moduleMetadata.declaredDirectives[0];
const directiveMetadata = resolver.getNonNormalizedDirectiveMetadata(directive.reference)!;
expect(directiveMetadata).toBeTruthy();
const {metadata} = directiveMetadata;
const diDeps: CompileDiDependencyMetadata[] = metadata.type.diDeps;
// 'null' does not conform to the shape of `CompileDiDependencyMetadata`
expect(diDeps.every(d => d !== null)).toBe(true);
}));
it(`should throw an error when a Directive is added to module's bootstrap list`,
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@Directive({selector: 'directive'})
class MyDirective {
}
@NgModule({declarations: [], bootstrap: [MyDirective]})
class ModuleWithDirectiveInBootstrap {
}
expect(() => resolver.getNgModuleMetadata(ModuleWithDirectiveInBootstrap))
.toThrowError(`MyDirective cannot be used as an entry component.`);
}));
it(`should not throw an error when a Component is added to module's bootstrap list`,
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@Component({template: ''})
class MyComp {
}
@NgModule({declarations: [MyComp], bootstrap: [MyComp]})
class ModuleWithComponentInBootstrap {
}
expect(() => resolver.getNgModuleMetadata(ModuleWithComponentInBootstrap)).not.toThrow();
}));
// #20049
it('should throw a reasonable message when an invalid import is given',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@NgModule({imports: [{ngModule: true as any}]})
class InvalidModule {
}
expect(() => {
resolver.getNgModuleMetadata(InvalidModule);
})
.toThrowError(
`Unexpected value '[object Object]' imported by the module 'InvalidModule'. Please add a @NgModule annotation.`);
}));
});
it('should dedupe declarations in NgModule',
inject([CompileMetadataResolver], (resolver: CompileMetadataResolver) => {
@Component({template: ''})
class MyComp {
}
@NgModule({declarations: [MyComp, MyComp]})
class MyModule {
}
const modMeta = resolver.getNgModuleMetadata(MyModule)!;
expect(modMeta.declaredDirectives.length).toBe(1);
expect(modMeta.declaredDirectives[0].reference).toBe(MyComp);
}));
}
@Component({selector: 'someComponent', template: ''})
class ComponentWithoutModuleId {
}
@Component({selector: 'someComponent', template: '', moduleId: <any>0})
class ComponentWithInvalidModuleId {
}
@Component({
selector: 'someSelector',
templateUrl: 'someTemplateUrl',
styleUrls: ['someStyleUrl'],
})
class ComponentWithExternalResources {
}
@Component({
selector: 'someSelector',
inputs: ['someProp'],
outputs: ['someEvent'],
host: {
'[someHostProp]': 'someHostPropExpr',
'(someHostListener)': 'someHostListenerExpr',
'someHostAttr': 'someHostAttrValue'
},
exportAs: 'someExportAs',
moduleId: 'someModuleId',
changeDetection: ChangeDetectionStrategy.Default,
template: 'someTemplate',
encapsulation: ViewEncapsulation.Emulated,
styles: ['someStyle'],
interpolation: ['{{', '}}']
})
class ComponentWithEverythingInline implements OnChanges, OnInit, DoCheck, OnDestroy,
AfterContentInit, AfterContentChecked, AfterViewInit,
AfterViewChecked {
ngOnChanges(changes: SimpleChanges): void {}
ngOnInit(): void {}
ngDoCheck(): void {}
ngOnDestroy(): void {}
ngAfterContentInit(): void {}
ngAfterContentChecked(): void {}
ngAfterViewInit(): void {}
ngAfterViewChecked(): void {}
}
@Component({selector: 'my-broken-comp', template: ''})
class MyBrokenComp1 {
constructor(public dependency: any) {}
}
class NonAnnotatedService {
constructor(dep: any) {}
}
@Component({selector: 'my-broken-comp', template: '', providers: [NonAnnotatedService]})
class MyBrokenComp2 {
constructor(dependency: NonAnnotatedService) {}
}
@Injectable()
class SimpleService {
}
@Component({selector: 'my-broken-comp', template: '', providers: [SimpleService, null!, [null]]})
class MyBrokenComp3 {
}
@Component(
{selector: 'my-broken-comp', template: '', viewProviders: [null!, SimpleService, [null]]})
class MyBrokenComp4 {
}
@Component({selector: 'someSelector', template: '', interpolation: [' ', ' ']})
class ComponentWithInvalidInterpolation1 {
}
@Component({selector: 'someSelector', template: '', interpolation: ['{', '}']})
class ComponentWithInvalidInterpolation2 {
}
@Component({selector: 'someSelector', template: '', interpolation: ['<%', '%>']})
class ComponentWithInvalidInterpolation3 {
}
@Component({selector: 'someSelector', template: '', interpolation: ['&#', '}}']})
class ComponentWithInvalidInterpolation4 {
}
@Component({selector: 'someSelector', template: '', interpolation: ['&lbrace;', '}}']})
class ComponentWithInvalidInterpolation5 {
}