import { ddescribe, describe, xdescribe, it, iit, xit, expect, beforeEach, afterEach, AsyncTestCompleter, inject, beforeEachProviders } from 'angular2/testing_internal'; import {PromiseWrapper} from 'angular2/src/facade/async'; import {Type, isPresent, isBlank, stringify, isString, IS_DART} from 'angular2/src/facade/lang'; import { MapWrapper, SetWrapper, ListWrapper, StringMapWrapper } from 'angular2/src/facade/collection'; import {RuntimeMetadataResolver} from 'angular2/src/compiler/runtime_metadata'; import { TemplateCompiler, NormalizedComponentWithViewDirectives } from 'angular2/src/compiler/template_compiler'; import {CompileDirectiveMetadata} from 'angular2/src/compiler/directive_metadata'; import {evalModule} from './eval_module'; import {SourceModule, moduleRef} from 'angular2/src/compiler/source_module'; import {XHR} from 'angular2/src/compiler/xhr'; import {MockXHR} from 'angular2/src/compiler/xhr_mock'; import {SpyRootRenderer, SpyRenderer, SpyAppViewManager} from '../core/spies'; import {ViewEncapsulation} from 'angular2/src/core/metadata/view'; import {AppView, AppProtoView} from 'angular2/src/core/linker/view'; import {AppElement} from 'angular2/src/core/linker/element'; import {Locals, ChangeDetectorGenConfig} from 'angular2/src/core/change_detection/change_detection'; import {Component, Directive, provide, RenderComponentType} from 'angular2/core'; import {TEST_PROVIDERS} from './test_bindings'; import { codeGenValueFn, codeGenFnHeader, codeGenExportVariable, MODULE_SUFFIX } from 'angular2/src/compiler/util'; import {PipeTransform, WrappedValue, Injectable, Pipe} from 'angular2/core'; // Attention: This path has to point to this test file! const THIS_MODULE_ID = 'angular2/test/compiler/template_compiler_spec'; var THIS_MODULE_REF = moduleRef(`package:${THIS_MODULE_ID}${MODULE_SUFFIX}`); var REFLECTOR_MODULE_REF = moduleRef(`package:angular2/src/core/reflection/reflection${MODULE_SUFFIX}`); var REFLECTION_CAPS_MODULE_REF = moduleRef(`package:angular2/src/core/reflection/reflection_capabilities${MODULE_SUFFIX}`); export function main() { // Dart's isolate support is broken, and these tests will be obsolote soon with // https://github.com/angular/angular/issues/6270 if (IS_DART) { return; } describe('TemplateCompiler', () => { var compiler: TemplateCompiler; var runtimeMetadataResolver: RuntimeMetadataResolver; beforeEachProviders(() => TEST_PROVIDERS); beforeEach(inject([TemplateCompiler, RuntimeMetadataResolver], (_compiler, _runtimeMetadataResolver) => { compiler = _compiler; runtimeMetadataResolver = _runtimeMetadataResolver; })); describe('compile templates', () => { function runTests(compile: (components: Type[]) => Promise) { it('should throw for non components', inject([AsyncTestCompleter], (async) => { PromiseWrapper.catchError( PromiseWrapper.wrap(() => compile([NonComponent])), (error): any => { expect(error.message) .toEqual( `Could not compile '${stringify(NonComponent)}' because it is not a component.`); async.done(); }); })); it('should compile host components', inject([AsyncTestCompleter], (async) => { compile([CompWithBindingsAndStylesAndPipes]) .then((humanizedView) => { expect(humanizedView['styles']).toEqual([]); expect(humanizedView['elements']).toEqual(['']); expect(humanizedView['pipes']).toEqual({}); expect(humanizedView['cd']).toEqual(['prop(title)=someHostValue']); async.done(); }); })); it('should compile nested components', inject([AsyncTestCompleter], (async) => { compile([CompWithBindingsAndStylesAndPipes]) .then((humanizedView) => { var componentView = humanizedView['componentViews'][0]; expect(componentView['styles']).toEqual(['div {color: red}']); expect(componentView['elements']).toEqual(['']); expect(componentView['pipes']).toEqual({'uppercase': stringify(UpperCasePipe)}); expect(componentView['cd']).toEqual(['prop(href)=SOMECTXVALUE']); async.done(); }); })); it('should compile components at various nesting levels', inject([AsyncTestCompleter], (async) => { compile([CompWith2NestedComps, Comp1, Comp2]) .then((humanizedView) => { expect(humanizedView['elements']).toEqual(['']); expect(humanizedView['componentViews'][0]['elements']) .toEqual(['', '']); expect(humanizedView['componentViews'][0]['componentViews'][0]['elements']) .toEqual(['', '']); expect(humanizedView['componentViews'][0]['componentViews'][1]['elements']) .toEqual(['']); async.done(); }); })); it('should compile recursive components', inject([AsyncTestCompleter], (async) => { compile([TreeComp]) .then((humanizedView) => { expect(humanizedView['elements']).toEqual(['']); expect(humanizedView['componentViews'][0]['embeddedViews'][0]['elements']) .toEqual(['']); expect(humanizedView['componentViews'][0]['embeddedViews'][0]['componentViews'] [0]['embeddedViews'][0]['elements']) .toEqual(['']); async.done(); }); })); it('should compile embedded templates', inject([AsyncTestCompleter], (async) => { compile([CompWithEmbeddedTemplate]) .then((humanizedView) => { var embeddedView = humanizedView['componentViews'][0]['embeddedViews'][0]; expect(embeddedView['elements']).toEqual(['']); expect(embeddedView['cd']).toEqual(['prop(href)=someEmbeddedValue']); async.done(); }); })); it('should dedup directives', inject([AsyncTestCompleter], (async) => { compile([CompWithDupDirectives, TreeComp]) .then((humanizedView) => { expect(humanizedView['componentViews'][0]['componentViews'].length).toBe(1); async.done(); }); })); } describe('compileHostComponentRuntime', () => { function compile(components: Type[]): Promise { return compiler.compileHostComponentRuntime(components[0]) .then((compiledHostTemplate) => humanizeViewFactory(compiledHostTemplate.viewFactory)); } describe('no jit', () => { beforeEachProviders(() => [ provide(ChangeDetectorGenConfig, {useValue: new ChangeDetectorGenConfig(true, false, false)}) ]); runTests(compile); }); describe('jit', () => { beforeEachProviders(() => [ provide(ChangeDetectorGenConfig, {useValue: new ChangeDetectorGenConfig(true, false, true)}) ]); runTests(compile); }); it('should cache components for parallel requests', inject([AsyncTestCompleter, XHR], (async, xhr: MockXHR) => { // Expecting only one xhr... xhr.expect('package:angular2/test/compiler/compUrl.html', ''); PromiseWrapper.all([compile([CompWithTemplateUrl]), compile([CompWithTemplateUrl])]) .then((humanizedViews) => { expect(humanizedViews[0]['componentViews'][0]['elements']).toEqual(['']); expect(humanizedViews[1]['componentViews'][0]['elements']).toEqual(['']); async.done(); }); xhr.flush(); })); it('should cache components for sequential requests', inject([AsyncTestCompleter, XHR], (async, xhr: MockXHR) => { // Expecting only one xhr... xhr.expect('package:angular2/test/compiler/compUrl.html', ''); compile([CompWithTemplateUrl]) .then((humanizedView0) => { return compile([CompWithTemplateUrl]) .then((humanizedView1) => { expect(humanizedView0['componentViews'][0]['elements']).toEqual(['']); expect(humanizedView1['componentViews'][0]['elements']).toEqual(['']); async.done(); }); }); xhr.flush(); })); it('should allow to clear the cache', inject([AsyncTestCompleter, XHR], (async, xhr: MockXHR) => { xhr.expect('package:angular2/test/compiler/compUrl.html', ''); compile([CompWithTemplateUrl]) .then((humanizedView) => { compiler.clearCache(); xhr.expect('package:angular2/test/compiler/compUrl.html', ''); var result = compile([CompWithTemplateUrl]); xhr.flush(); return result; }) .then((humanizedView) => { expect(humanizedView['componentViews'][0]['elements']).toEqual(['']); async.done(); }); xhr.flush(); })); }); describe('compileTemplatesCodeGen', () => { function normalizeComponent( component: Type): Promise { var compAndViewDirMetas = [runtimeMetadataResolver.getDirectiveMetadata(component)].concat( runtimeMetadataResolver.getViewDirectivesMetadata(component)); var upperCasePipeMeta = runtimeMetadataResolver.getPipeMetadata(UpperCasePipe); upperCasePipeMeta.type.moduleUrl = `package:${THIS_MODULE_ID}${MODULE_SUFFIX}`; return PromiseWrapper.all(compAndViewDirMetas.map( meta => compiler.normalizeDirectiveMetadata(meta))) .then((normalizedCompAndViewDirMetas: CompileDirectiveMetadata[]) => new NormalizedComponentWithViewDirectives( normalizedCompAndViewDirMetas[0], normalizedCompAndViewDirMetas.slice(1), [upperCasePipeMeta])); } function compile(components: Type[]): Promise { return PromiseWrapper.all(components.map(normalizeComponent)) .then((normalizedCompWithViewDirMetas: NormalizedComponentWithViewDirectives[]) => { var sourceModule = compiler.compileTemplatesCodeGen(normalizedCompWithViewDirMetas); var sourceWithImports = testableTemplateModule(sourceModule, normalizedCompWithViewDirMetas[0].component) .getSourceWithImports(); return evalModule(sourceWithImports.source, sourceWithImports.imports, null); }); } runTests(compile); }); }); describe('normalizeDirectiveMetadata', () => { it('should return the given DirectiveMetadata for non components', inject([AsyncTestCompleter], (async) => { var meta = runtimeMetadataResolver.getDirectiveMetadata(NonComponent); compiler.normalizeDirectiveMetadata(meta).then(normMeta => { expect(normMeta).toBe(meta); async.done(); }); })); it('should normalize the template', inject([AsyncTestCompleter, XHR], (async, xhr: MockXHR) => { xhr.expect('package:angular2/test/compiler/compUrl.html', 'loadedTemplate'); compiler.normalizeDirectiveMetadata( runtimeMetadataResolver.getDirectiveMetadata(CompWithTemplateUrl)) .then((meta: CompileDirectiveMetadata) => { expect(meta.template.template).toEqual('loadedTemplate'); async.done(); }); xhr.flush(); })); it('should copy all the other fields', inject([AsyncTestCompleter], (async) => { var meta = runtimeMetadataResolver.getDirectiveMetadata(CompWithBindingsAndStylesAndPipes); compiler.normalizeDirectiveMetadata(meta).then((normMeta: CompileDirectiveMetadata) => { expect(normMeta.type).toEqual(meta.type); expect(normMeta.isComponent).toEqual(meta.isComponent); expect(normMeta.dynamicLoadable).toEqual(meta.dynamicLoadable); expect(normMeta.selector).toEqual(meta.selector); expect(normMeta.exportAs).toEqual(meta.exportAs); expect(normMeta.changeDetection).toEqual(meta.changeDetection); expect(normMeta.inputs).toEqual(meta.inputs); expect(normMeta.outputs).toEqual(meta.outputs); expect(normMeta.hostListeners).toEqual(meta.hostListeners); expect(normMeta.hostProperties).toEqual(meta.hostProperties); expect(normMeta.hostAttributes).toEqual(meta.hostAttributes); expect(normMeta.lifecycleHooks).toEqual(meta.lifecycleHooks); async.done(); }); })); }); describe('compileStylesheetCodeGen', () => { it('should compile stylesheets into code', inject([AsyncTestCompleter], (async) => { var cssText = 'div {color: red}'; var sourceModule = compiler.compileStylesheetCodeGen('package:someModuleUrl', cssText)[0]; var sourceWithImports = testableStylesModule(sourceModule).getSourceWithImports(); evalModule(sourceWithImports.source, sourceWithImports.imports, null) .then(loadedCssText => { expect(loadedCssText).toEqual([cssText]); async.done(); }); })); }); }); } @Pipe({name: 'uppercase'}) @Injectable() export class UpperCasePipe implements PipeTransform { transform(value: string, args: any[] = null): string { return value.toUpperCase(); } } @Component({ selector: 'comp-a', host: {'[title]': '\'someHostValue\''}, moduleId: THIS_MODULE_ID, exportAs: 'someExportAs', template: '', styles: ['div {color: red}'], encapsulation: ViewEncapsulation.None, pipes: [UpperCasePipe] }) export class CompWithBindingsAndStylesAndPipes { } @Component({ selector: 'tree', moduleId: THIS_MODULE_ID, template: '', directives: [TreeComp], encapsulation: ViewEncapsulation.None }) export class TreeComp { } @Component({ selector: 'comp-wit-dup-tpl', moduleId: THIS_MODULE_ID, template: '', directives: [TreeComp, TreeComp], encapsulation: ViewEncapsulation.None }) export class CompWithDupDirectives { } @Component({ selector: 'comp-url', moduleId: THIS_MODULE_ID, templateUrl: 'compUrl.html', encapsulation: ViewEncapsulation.None }) export class CompWithTemplateUrl { } @Component({ selector: 'comp-tpl', moduleId: THIS_MODULE_ID, template: '', encapsulation: ViewEncapsulation.None }) export class CompWithEmbeddedTemplate { } @Directive({selector: 'plain'}) export class NonComponent { } @Component({ selector: 'comp2', moduleId: THIS_MODULE_ID, template: '', encapsulation: ViewEncapsulation.None }) export class Comp2 { } @Component({ selector: 'comp1', moduleId: THIS_MODULE_ID, template: ', ', encapsulation: ViewEncapsulation.None, directives: [Comp2] }) export class Comp1 { } @Component({ selector: 'comp-with-2nested', moduleId: THIS_MODULE_ID, template: ', ', encapsulation: ViewEncapsulation.None, directives: [Comp1, Comp2] }) export class CompWith2NestedComps { } function testableTemplateModule(sourceModule: SourceModule, normComp: CompileDirectiveMetadata): SourceModule { var testableSource = ` ${sourceModule.sourceWithModuleRefs} ${codeGenFnHeader(['_'], '_run')}{ ${REFLECTOR_MODULE_REF}reflector.reflectionCapabilities = new ${REFLECTION_CAPS_MODULE_REF}ReflectionCapabilities(); return ${THIS_MODULE_REF}humanizeViewFactory(hostViewFactory_${normComp.type.name}.viewFactory); } ${codeGenExportVariable('run')}_run;`; return new SourceModule(sourceModule.moduleUrl, testableSource); } function testableStylesModule(sourceModule: SourceModule): SourceModule { var testableSource = `${sourceModule.sourceWithModuleRefs} ${codeGenValueFn(['_'], 'STYLES', '_run')}; ${codeGenExportVariable('run')}_run;`; return new SourceModule(sourceModule.moduleUrl, testableSource); } function humanizeView(view: AppView, cachedResults: Map): {[key: string]: any} { var result = cachedResults.get(view.proto); if (isPresent(result)) { return result; } result = {}; // fill the cache early to break cycles. cachedResults.set(view.proto, result); view.changeDetector.detectChanges(); var pipes = {}; if (isPresent(view.proto.protoPipes)) { StringMapWrapper.forEach(view.proto.protoPipes.config, (pipeProvider, pipeName) => { pipes[pipeName] = stringify(pipeProvider.key.token); }); } var componentViews = []; var embeddedViews = []; view.appElements.forEach((appElement) => { if (isPresent(appElement.componentView)) { componentViews.push(humanizeView(appElement.componentView, cachedResults)); } else if (isPresent(appElement.embeddedViewFactory)) { embeddedViews.push( humanizeViewFactory(appElement.embeddedViewFactory, appElement, cachedResults)); } }); result['styles'] = (view.renderer).styles; result['elements'] = (view.renderer).elements; result['pipes'] = pipes; result['cd'] = (view.renderer).props; result['componentViews'] = componentViews; result['embeddedViews'] = embeddedViews; return result; } // Attention: read by eval! export function humanizeViewFactory( viewFactory: Function, containerAppElement: AppElement = null, cachedResults: Map = null): {[key: string]: any} { if (isBlank(cachedResults)) { cachedResults = new Map(); } var viewManager = new SpyAppViewManager(); viewManager.spy('createRenderComponentType') .andCallFake((encapsulation: ViewEncapsulation, styles: Array) => { return new RenderComponentType('someId', encapsulation, styles); }); var view: AppView = viewFactory(new RecordingRenderer([]), viewManager, containerAppElement, [], null, null, null); return humanizeView(view, cachedResults); } class RecordingRenderer extends SpyRenderer { props: string[] = []; elements: string[] = []; constructor(public styles: string[]) { super(); this.spy('renderComponent') .andCallFake((componentProto) => new RecordingRenderer(componentProto.styles)); this.spy('setElementProperty') .andCallFake((el, prop, value) => { this.props.push(`prop(${prop})=${value}`); }); this.spy('createElement') .andCallFake((parent, elName) => { this.elements.push(`<${elName}>`); }); } }