angular-docs-cn/packages/compiler/test/metadata_resolver_spec.ts

554 lines
23 KiB
TypeScript
Raw Normal View History

/**
* @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';
2017-03-02 12:37:01 -05:00
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 {
}
2016-06-21 19:55:17 -04:00
@Component({selector: 'someSelector', template: '', interpolation: ['&#', '}}']})
class ComponentWithInvalidInterpolation4 {
}
@Component({selector: 'someSelector', template: '', interpolation: ['&lbrace;', '}}']})
class ComponentWithInvalidInterpolation5 {
}