diff --git a/packages/core/testing/src/r3_test_bed.ts b/packages/core/testing/src/r3_test_bed.ts index 2708ad9be9..115609747e 100644 --- a/packages/core/testing/src/r3_test_bed.ts +++ b/packages/core/testing/src/r3_test_bed.ts @@ -16,6 +16,16 @@ import {ComponentFixtureAutoDetect, ComponentFixtureNoNgZone, TestBedStatic, Tes let _nextRootElementId = 0; +const EMPTY_ARRAY: Type[] = []; + +// Resolvers for Angular decorators +type Resolvers = { + module: Resolver, + component: Resolver, + directive: Resolver, + pipe: Resolver, +}; + /** * @description * Configures and initializes environment for unit testing and provides methods for @@ -174,6 +184,7 @@ export class TestBedRender3 implements Injector, TestBed { private _pipeOverrides: [Type, MetadataOverride][] = []; private _providerOverrides: Provider[] = []; private _rootProviderOverrides: Provider[] = []; + private _providerOverridesByToken: Map = new Map(); // test module configuration private _providers: Provider[] = []; @@ -229,6 +240,7 @@ export class TestBedRender3 implements Injector, TestBed { this._pipeOverrides = []; this._providerOverrides = []; this._rootProviderOverrides = []; + this._providerOverridesByToken.clear(); // reset test module config this._providers = []; @@ -322,17 +334,21 @@ export class TestBedRender3 implements Injector, TestBed { */ overrideProvider(token: any, provider: {useFactory?: Function, useValue?: any, deps?: any[]}): void { + const providerDef = provider.useFactory ? + {provide: token, useFactory: provider.useFactory, deps: provider.deps || []} : + {provide: token, useValue: provider.useValue}; + let injectableDef: InjectableDef|null; const isRoot = (typeof token !== 'string' && (injectableDef = getInjectableDef(token)) && injectableDef.providedIn === 'root'); - const overrides = isRoot ? this._rootProviderOverrides : this._providerOverrides; + const overridesBucket = isRoot ? this._rootProviderOverrides : this._providerOverrides; + overridesBucket.push(providerDef); - if (provider.useFactory) { - overrides.push({provide: token, useFactory: provider.useFactory, deps: provider.deps || []}); - } else { - overrides.push({provide: token, useValue: provider.useValue}); - } + // keep all overrides grouped by token as well for fast lookups using token + const overridesForToken = this._providerOverridesByToken.get(token) || []; + overridesForToken.push(providerDef); + this._providerOverridesByToken.set(token, overridesForToken); } /** @@ -387,8 +403,7 @@ export class TestBedRender3 implements Injector, TestBed { const resolvers = this._getResolvers(); const testModuleType = this._createTestModule(); - - compileNgModule(testModuleType, resolvers); + this._compileNgModule(testModuleType, resolvers); const parentInjector = this.platform.injector; this._moduleRef = new NgModuleRef(testModuleType, parentInjector); @@ -399,6 +414,14 @@ export class TestBedRender3 implements Injector, TestBed { this._instantiated = true; } + // get overrides for a specific provider (if any) + private _getProviderOverrides(provider: any) { + const token = typeof provider === 'object' && provider.hasOwnProperty('provide') ? + provider.provide : + provider; + return this._providerOverridesByToken.get(token) || []; + } + // creates resolvers taking overrides into account private _getResolvers() { const module = new NgModuleResolver(); @@ -448,6 +471,150 @@ export class TestBedRender3 implements Injector, TestBed { return DynamicTestModule as NgModuleType; } + + private _getMetaWithOverrides(meta: Component|Directive|NgModule) { + if (meta.providers && meta.providers.length) { + const overrides = + flatten(meta.providers, (provider: any) => this._getProviderOverrides(provider)); + if (overrides.length) { + return {...meta, providers: [...meta.providers, ...overrides]}; + } + } + return meta; + } + + private _compileNgModule(moduleType: NgModuleType, resolvers: Resolvers): void { + const ngModule = resolvers.module.resolve(moduleType); + + if (ngModule === null) { + throw new Error(`${stringify(moduleType)} has not @NgModule annotation`); + } + + const metadata = this._getMetaWithOverrides(ngModule); + compileNgModuleDefs(moduleType, metadata); + + const declarations: Type[] = flatten(ngModule.declarations || EMPTY_ARRAY); + const compiledComponents: Type[] = []; + + // Compile the components, directives and pipes declared by this module + declarations.forEach(declaration => { + const component = resolvers.component.resolve(declaration); + if (component) { + const metadata = this._getMetaWithOverrides(component); + compileComponent(declaration, metadata); + compiledComponents.push(declaration); + return; + } + + const directive = resolvers.directive.resolve(declaration); + if (directive) { + const metadata = this._getMetaWithOverrides(directive); + compileDirective(declaration, metadata); + return; + } + + const pipe = resolvers.pipe.resolve(declaration); + if (pipe) { + compilePipe(declaration, pipe); + return; + } + }); + + // Compile transitive modules, components, directives and pipes + const transitiveScope = this._transitiveScopesFor(moduleType, resolvers); + compiledComponents.forEach( + cmp => patchComponentDefWithScope((cmp as any).ngComponentDef, transitiveScope)); + } + + /** + * Compute the pair of transitive scopes (compilation scope and exported scope) for a given + * module. + * + * This operation is memoized and the result is cached on the module's definition. It can be + * called on modules with components that have not fully compiled yet, but the result should not + * be used until they have. + */ + private _transitiveScopesFor(moduleType: Type, resolvers: Resolvers): + NgModuleTransitiveScopes { + if (!isNgModule(moduleType)) { + throw new Error(`${moduleType.name} does not have an ngModuleDef`); + } + const def = moduleType.ngModuleDef; + + if (def.transitiveCompileScopes !== null) { + return def.transitiveCompileScopes; + } + + const scopes: NgModuleTransitiveScopes = { + compilation: { + directives: new Set(), + pipes: new Set(), + }, + exported: { + directives: new Set(), + pipes: new Set(), + }, + }; + + def.declarations.forEach(declared => { + const declaredWithDefs = declared as Type& { ngPipeDef?: any; }; + + if (declaredWithDefs.ngPipeDef !== undefined) { + scopes.compilation.pipes.add(declared); + } else { + scopes.compilation.directives.add(declared); + } + }); + + def.imports.forEach((imported: NgModuleType) => { + const ngModule = resolvers.module.resolve(imported); + + if (ngModule === null) { + throw new Error(`Importing ${imported.name} which does not have an @ngModule`); + } else { + this._compileNgModule(imported, resolvers); + } + + // When this module imports another, the imported module's exported directives and pipes are + // added to the compilation scope of this module. + const importedScope = this._transitiveScopesFor(imported, resolvers); + importedScope.exported.directives.forEach(entry => scopes.compilation.directives.add(entry)); + importedScope.exported.pipes.forEach(entry => scopes.compilation.pipes.add(entry)); + }); + + def.exports.forEach((exported: Type) => { + const exportedTyped = exported as Type& { + // Components, Directives, NgModules, and Pipes can all be exported. + ngComponentDef?: any; + ngDirectiveDef?: any; + ngModuleDef?: NgModuleDef; + ngPipeDef?: any; + }; + + // Either the type is a module, a pipe, or a component/directive (which may not have an + // ngComponentDef as it might be compiled asynchronously). + if (isNgModule(exportedTyped)) { + // When this module exports another, the exported module's exported directives and pipes are + // added to both the compilation and exported scopes of this module. + const exportedScope = this._transitiveScopesFor(exportedTyped, resolvers); + exportedScope.exported.directives.forEach(entry => { + scopes.compilation.directives.add(entry); + scopes.exported.directives.add(entry); + }); + exportedScope.exported.pipes.forEach(entry => { + scopes.compilation.pipes.add(entry); + scopes.exported.pipes.add(entry); + }); + } else if (exportedTyped.ngPipeDef !== undefined) { + scopes.exported.pipes.add(exportedTyped); + } else { + scopes.exported.directives.add(exportedTyped); + } + }); + + def.transitiveCompileScopes = scopes; + return scopes; + } } let testBed: TestBedRender3; @@ -458,9 +625,10 @@ export function _getTestBedRender3(): TestBedRender3 { const OWNER_MODULE = '__NG_MODULE__'; /** - * This function clears the OWNER_MODULE property from the Types. This is set in r3/jit/modules.ts. - * It is common for the same Type to be compiled in different tests. If we don't clear this we will - * get errors which will complain that the same Component/Directive is in more than one NgModule. + * This function clears the OWNER_MODULE property from the Types. This is set in + * r3/jit/modules.ts. It is common for the same Type to be compiled in different tests. If we don't + * clear this we will get errors which will complain that the same Component/Directive is in more + * than one NgModule. */ function clearNgModules(type: Type) { if (type.hasOwnProperty(OWNER_MODULE)) { @@ -468,156 +636,13 @@ function clearNgModules(type: Type) { } } - -// Module compiler - -const EMPTY_ARRAY: Type[] = []; - -// Resolvers for Angular decorators -type Resolvers = { - module: Resolver, - component: Resolver, - directive: Resolver, - pipe: Resolver, -}; - -function compileNgModule(moduleType: NgModuleType, resolvers: Resolvers): void { - const ngModule = resolvers.module.resolve(moduleType); - - if (ngModule === null) { - throw new Error(`${stringify(moduleType)} has not @NgModule annotation`); - } - - compileNgModuleDefs(moduleType, ngModule); - - const declarations: Type[] = flatten(ngModule.declarations || EMPTY_ARRAY); - - const compiledComponents: Type[] = []; - - // Compile the components, directives and pipes declared by this module - declarations.forEach(declaration => { - const component = resolvers.component.resolve(declaration); - if (component) { - compileComponent(declaration, component); - compiledComponents.push(declaration); - return; - } - - const directive = resolvers.directive.resolve(declaration); - if (directive) { - compileDirective(declaration, directive); - return; - } - - const pipe = resolvers.pipe.resolve(declaration); - if (pipe) { - compilePipe(declaration, pipe); - return; - } - }); - - // Compile transitive modules, components, directives and pipes - const transitiveScope = transitiveScopesFor(moduleType, resolvers); - compiledComponents.forEach( - cmp => patchComponentDefWithScope((cmp as any).ngComponentDef, transitiveScope)); -} - -/** - * Compute the pair of transitive scopes (compilation scope and exported scope) for a given module. - * - * This operation is memoized and the result is cached on the module's definition. It can be called - * on modules with components that have not fully compiled yet, but the result should not be used - * until they have. - */ -function transitiveScopesFor( - moduleType: Type, resolvers: Resolvers): NgModuleTransitiveScopes { - if (!isNgModule(moduleType)) { - throw new Error(`${moduleType.name} does not have an ngModuleDef`); - } - const def = moduleType.ngModuleDef; - - if (def.transitiveCompileScopes !== null) { - return def.transitiveCompileScopes; - } - - const scopes: NgModuleTransitiveScopes = { - compilation: { - directives: new Set(), - pipes: new Set(), - }, - exported: { - directives: new Set(), - pipes: new Set(), - }, - }; - - def.declarations.forEach(declared => { - const declaredWithDefs = declared as Type& { ngPipeDef?: any; }; - - if (declaredWithDefs.ngPipeDef !== undefined) { - scopes.compilation.pipes.add(declared); - } else { - scopes.compilation.directives.add(declared); - } - }); - - def.imports.forEach((imported: NgModuleType) => { - const ngModule = resolvers.module.resolve(imported); - - if (ngModule === null) { - throw new Error(`Importing ${imported.name} which does not have an @ngModule`); - } else { - compileNgModule(imported, resolvers); - } - - // When this module imports another, the imported module's exported directives and pipes are - // added to the compilation scope of this module. - const importedScope = transitiveScopesFor(imported, resolvers); - importedScope.exported.directives.forEach(entry => scopes.compilation.directives.add(entry)); - importedScope.exported.pipes.forEach(entry => scopes.compilation.pipes.add(entry)); - }); - - def.exports.forEach((exported: Type) => { - const exportedTyped = exported as Type& { - // Components, Directives, NgModules, and Pipes can all be exported. - ngComponentDef?: any; - ngDirectiveDef?: any; - ngModuleDef?: NgModuleDef; - ngPipeDef?: any; - }; - - // Either the type is a module, a pipe, or a component/directive (which may not have an - // ngComponentDef as it might be compiled asynchronously). - if (isNgModule(exportedTyped)) { - // When this module exports another, the exported module's exported directives and pipes are - // added to both the compilation and exported scopes of this module. - const exportedScope = transitiveScopesFor(exportedTyped, resolvers); - exportedScope.exported.directives.forEach(entry => { - scopes.compilation.directives.add(entry); - scopes.exported.directives.add(entry); - }); - exportedScope.exported.pipes.forEach(entry => { - scopes.compilation.pipes.add(entry); - scopes.exported.pipes.add(entry); - }); - } else if (exportedTyped.ngPipeDef !== undefined) { - scopes.exported.pipes.add(exportedTyped); - } else { - scopes.exported.directives.add(exportedTyped); - } - }); - - def.transitiveCompileScopes = scopes; - return scopes; -} - -function flatten(values: any[]): T[] { +function flatten(values: any[], mapFn?: (value: T) => any): T[] { const out: T[] = []; values.forEach(value => { if (Array.isArray(value)) { - out.push(...flatten(value)); + out.push(...flatten(value, mapFn)); } else { - out.push(value); + out.push(mapFn ? mapFn(value) : value); } }); return out; diff --git a/packages/platform-browser/test/testing_public_spec.ts b/packages/platform-browser/test/testing_public_spec.ts index e41fbcbbcb..b2943bce3d 100644 --- a/packages/platform-browser/test/testing_public_spec.ts +++ b/packages/platform-browser/test/testing_public_spec.ts @@ -458,7 +458,7 @@ class CompWithUrlTemplate { expect(TestBed.get('a')).toBe('mockA: depValue'); }); - fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)') + fixmeIvy('FW-855: TestBed.get(Compiler) should return TestBed-specific Compiler instance') .it('should support SkipSelf', () => { @NgModule({ providers: [ @@ -553,147 +553,137 @@ class CompWithUrlTemplate { }); describe('in Components', () => { - fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)') - .it('should support useValue', () => { - @Component({ - template: '', - providers: [ - {provide: 'a', useValue: 'aValue'}, - ] - }) - class MComp { - } + it('should support useValue', () => { + @Component({ + template: '', + providers: [ + {provide: 'a', useValue: 'aValue'}, + ] + }) + class MComp { + } - TestBed.overrideProvider('a', {useValue: 'mockValue'}); - const ctx = - TestBed.configureTestingModule({declarations: [MComp]}).createComponent(MComp); + TestBed.overrideProvider('a', {useValue: 'mockValue'}); + const ctx = + TestBed.configureTestingModule({declarations: [MComp]}).createComponent(MComp); - expect(ctx.debugElement.injector.get('a')).toBe('mockValue'); - }); + expect(ctx.debugElement.injector.get('a')).toBe('mockValue'); + }); - fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)') - .it('should support useFactory', () => { - @Component({ - template: '', - providers: [ - {provide: 'dep', useValue: 'depValue'}, - {provide: 'a', useValue: 'aValue'}, - ] - }) - class MyComp { - } + it('should support useFactory', () => { + @Component({ + template: '', + providers: [ + {provide: 'dep', useValue: 'depValue'}, + {provide: 'a', useValue: 'aValue'}, + ] + }) + class MyComp { + } - TestBed.overrideProvider( - 'a', {useFactory: (dep: any) => `mockA: ${dep}`, deps: ['dep']}); - const ctx = TestBed.configureTestingModule({declarations: [MyComp]}) - .createComponent(MyComp); + TestBed.overrideProvider( + 'a', {useFactory: (dep: any) => `mockA: ${dep}`, deps: ['dep']}); + const ctx = + TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp); - expect(ctx.debugElement.injector.get('a')).toBe('mockA: depValue'); - }); + expect(ctx.debugElement.injector.get('a')).toBe('mockA: depValue'); + }); - fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)') - .it('should support @Optional without matches', () => { - @Component({ - template: '', - providers: [ - {provide: 'a', useValue: 'aValue'}, - ] - }) - class MyComp { - } + it('should support @Optional without matches', () => { + @Component({ + template: '', + providers: [ + {provide: 'a', useValue: 'aValue'}, + ] + }) + class MyComp { + } - TestBed.overrideProvider( - 'a', - {useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new Optional(), 'dep']]}); - const ctx = TestBed.configureTestingModule({declarations: [MyComp]}) - .createComponent(MyComp); + TestBed.overrideProvider( + 'a', {useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new Optional(), 'dep']]}); + const ctx = + TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp); - expect(ctx.debugElement.injector.get('a')).toBe('mockA: null'); - }); + expect(ctx.debugElement.injector.get('a')).toBe('mockA: null'); + }); - fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)') - .it('should support Optional with matches', () => { - @Component({ - template: '', - providers: [ - {provide: 'dep', useValue: 'depValue'}, - {provide: 'a', useValue: 'aValue'}, - ] - }) - class MyComp { - } + it('should support Optional with matches', () => { + @Component({ + template: '', + providers: [ + {provide: 'dep', useValue: 'depValue'}, + {provide: 'a', useValue: 'aValue'}, + ] + }) + class MyComp { + } - TestBed.overrideProvider( - 'a', - {useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new Optional(), 'dep']]}); - const ctx = TestBed.configureTestingModule({declarations: [MyComp]}) - .createComponent(MyComp); + TestBed.overrideProvider( + 'a', {useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new Optional(), 'dep']]}); + const ctx = + TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp); - expect(ctx.debugElement.injector.get('a')).toBe('mockA: depValue'); - }); + expect(ctx.debugElement.injector.get('a')).toBe('mockA: depValue'); + }); - fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)') - .it('should support SkipSelf', () => { - @Directive({ - selector: '[myDir]', - providers: [ - {provide: 'a', useValue: 'aValue'}, - {provide: 'dep', useValue: 'depValue'}, - ] - }) - class MyDir { - } + it('should support SkipSelf', () => { + @Directive({ + selector: '[myDir]', + providers: [ + {provide: 'a', useValue: 'aValue'}, + {provide: 'dep', useValue: 'depValue'}, + ] + }) + class MyDir { + } - @Component({ - template: '
', - providers: [ - {provide: 'dep', useValue: 'parentDepValue'}, - ] - }) - class MyComp { - } + @Component({ + template: '
', + providers: [ + {provide: 'dep', useValue: 'parentDepValue'}, + ] + }) + class MyComp { + } - TestBed.overrideProvider( - 'a', - {useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new SkipSelf(), 'dep']]}); - const ctx = TestBed.configureTestingModule({declarations: [MyComp, MyDir]}) - .createComponent(MyComp); - expect(ctx.debugElement.children[0].injector.get('a')) - .toBe('mockA: parentDepValue'); - }); + TestBed.overrideProvider( + 'a', {useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new SkipSelf(), 'dep']]}); + const ctx = TestBed.configureTestingModule({declarations: [MyComp, MyDir]}) + .createComponent(MyComp); + expect(ctx.debugElement.children[0].injector.get('a')).toBe('mockA: parentDepValue'); + }); - fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)') - .it('should support multiple providers in a template', () => { - @Directive({ - selector: '[myDir1]', - providers: [ - {provide: 'a', useValue: 'aValue1'}, - ] - }) - class MyDir1 { - } + it('should support multiple providers in a template', () => { + @Directive({ + selector: '[myDir1]', + providers: [ + {provide: 'a', useValue: 'aValue1'}, + ] + }) + class MyDir1 { + } - @Directive({ - selector: '[myDir2]', - providers: [ - {provide: 'a', useValue: 'aValue2'}, - ] - }) - class MyDir2 { - } + @Directive({ + selector: '[myDir2]', + providers: [ + {provide: 'a', useValue: 'aValue2'}, + ] + }) + class MyDir2 { + } - @Component({ - template: '
', - }) - class MyComp { - } + @Component({ + template: '
', + }) + class MyComp { + } - TestBed.overrideProvider('a', {useValue: 'mockA'}); - const ctx = TestBed.configureTestingModule({declarations: [MyComp, MyDir1, MyDir2]}) - .createComponent(MyComp); - expect(ctx.debugElement.children[0].injector.get('a')).toBe('mockA'); - expect(ctx.debugElement.children[1].injector.get('a')).toBe('mockA'); - }); + TestBed.overrideProvider('a', {useValue: 'mockA'}); + const ctx = TestBed.configureTestingModule({declarations: [MyComp, MyDir1, MyDir2]}) + .createComponent(MyComp); + expect(ctx.debugElement.children[0].injector.get('a')).toBe('mockA'); + expect(ctx.debugElement.children[1].injector.get('a')).toBe('mockA'); + }); describe('injecting eager providers into an eager overwritten provider', () => { @Component({ @@ -708,25 +698,23 @@ class CompWithUrlTemplate { constructor(@Inject('a') a: any, @Inject('b') b: any) {} } - fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)') - .it('should inject providers that were declared before it', () => { - TestBed.overrideProvider( - 'b', {useFactory: (a: string) => `mockB: ${a}`, deps: ['a']}); - const ctx = TestBed.configureTestingModule({declarations: [MyComp]}) - .createComponent(MyComp); + it('should inject providers that were declared before it', () => { + TestBed.overrideProvider( + 'b', {useFactory: (a: string) => `mockB: ${a}`, deps: ['a']}); + const ctx = + TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp); - expect(ctx.debugElement.injector.get('b')).toBe('mockB: aValue'); - }); + expect(ctx.debugElement.injector.get('b')).toBe('mockB: aValue'); + }); - fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)') - .it('should inject providers that were declared after it', () => { - TestBed.overrideProvider( - 'a', {useFactory: (b: string) => `mockA: ${b}`, deps: ['b']}); - const ctx = TestBed.configureTestingModule({declarations: [MyComp]}) - .createComponent(MyComp); + it('should inject providers that were declared after it', () => { + TestBed.overrideProvider( + 'a', {useFactory: (b: string) => `mockA: ${b}`, deps: ['b']}); + const ctx = + TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp); - expect(ctx.debugElement.injector.get('a')).toBe('mockA: bValue'); - }); + expect(ctx.debugElement.injector.get('a')).toBe('mockA: bValue'); + }); }); }); @@ -739,7 +727,7 @@ class CompWithUrlTemplate { }); describe('overrideTemplateUsingTestingModule', () => { - fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)') + fixmeIvy('FW-851: TestBed.overrideTemplateUsingTestingModule is not implemented') .it('should compile the template in the context of the testing module', () => { @Component({selector: 'comp', template: 'a'}) class MyComponent { @@ -768,7 +756,7 @@ class CompWithUrlTemplate { expect(testDir !.test).toBe('some prop'); }); - fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)') + fixmeIvy('FW-851: TestBed.overrideTemplateUsingTestingModule is not implemented') .it('should throw if the TestBed is already created', () => { @Component({selector: 'comp', template: 'a'}) class MyComponent { @@ -781,7 +769,7 @@ class CompWithUrlTemplate { /Cannot override template when the test module has already been instantiated/); }); - fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)') + fixmeIvy('FW-851: TestBed.overrideTemplateUsingTestingModule is not implemented') .it('should reset overrides when the testing module is resetted', () => { @Component({selector: 'comp', template: 'a'}) class MyComponent {