angular-docs-cn/modules/angular2/test/compiler/template_compiler_spec.ts

418 lines
16 KiB
TypeScript

import {
ddescribe,
describe,
xdescribe,
it,
iit,
xit,
expect,
beforeEach,
afterEach,
AsyncTestCompleter,
inject,
beforeEachProviders
} from 'angular2/testing_internal';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {Type, isPresent, isBlank, stringify, isString} from 'angular2/src/facade/lang';
import {MapWrapper, SetWrapper, ListWrapper} 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 {ViewEncapsulation} from 'angular2/src/core/metadata/view';
import {Locals} from 'angular2/src/core/change_detection/change_detection';
import {
CommandVisitor,
TextCmd,
NgContentCmd,
BeginElementCmd,
BeginComponentCmd,
EmbeddedTemplateCmd,
TemplateCmd,
visitAllCommands,
CompiledComponentTemplate
} from 'angular2/src/core/linker/template_commands';
import {Component, View, Directive, provide} from 'angular2/core';
import {TEST_PROVIDERS} from './test_bindings';
import {TestDispatcher, TestPipes} from './change_detector_mocks';
import {codeGenValueFn, codeGenExportVariable, MODULE_SUFFIX} from 'angular2/src/compiler/util';
// 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}`);
export function main() {
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) {
it('should throw for non components', inject([AsyncTestCompleter], (async) => {
PromiseWrapper.catchError(PromiseWrapper.wrap(() => compile([NonComponent])), (error) => {
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([CompWithBindingsAndStyles])
.then((humanizedTemplate) => {
expect(humanizedTemplate['styles']).toEqual([]);
expect(humanizedTemplate['commands'][0]).toEqual('<comp-a>');
expect(humanizedTemplate['cd']).toEqual(['elementProperty(title)=someDirValue']);
async.done();
});
}));
it('should compile nested components', inject([AsyncTestCompleter], (async) => {
compile([CompWithBindingsAndStyles])
.then((humanizedTemplate) => {
var nestedTemplate = humanizedTemplate['commands'][1];
expect(nestedTemplate['styles']).toEqual(['div {color: red}']);
expect(nestedTemplate['commands'][0]).toEqual('<a>');
expect(nestedTemplate['cd']).toEqual(['elementProperty(href)=someCtxValue']);
async.done();
});
}));
it('should compile recursive components', inject([AsyncTestCompleter], (async) => {
compile([TreeComp])
.then((humanizedTemplate) => {
expect(humanizedTemplate['commands'][0]).toEqual('<tree>');
expect(humanizedTemplate['commands'][1]['commands'][0]).toEqual('<tree>');
expect(humanizedTemplate['commands'][1]['commands'][1]['commands'][0])
.toEqual('<tree>');
async.done();
});
}));
it('should pass the right change detector to embedded templates',
inject([AsyncTestCompleter], (async) => {
compile([CompWithEmbeddedTemplate])
.then((humanizedTemplate) => {
expect(humanizedTemplate['commands'][1]['commands'][0]).toEqual('<template>');
expect(humanizedTemplate['commands'][1]['commands'][1]['cd'])
.toEqual(['elementProperty(href)=someCtxValue']);
async.done();
});
}));
it('should dedup directives', inject([AsyncTestCompleter], (async) => {
compile([CompWithDupDirectives, TreeComp])
.then((humanizedTemplate) => {
expect(humanizedTemplate['commands'][1]['commands'][0]).toEqual("<tree>");
async.done();
});
}));
}
describe('compileHostComponentRuntime', () => {
function compile(components: Type[]): Promise<any[]> {
return compiler.compileHostComponentRuntime(components[0])
.then((compiledHostTemplate) => humanizeTemplate(compiledHostTemplate.template));
}
runTests(compile);
it('should cache components for parallel requests',
inject([AsyncTestCompleter, XHR], (async, xhr: MockXHR) => {
xhr.expect('package:angular2/test/compiler/compUrl.html', 'a');
PromiseWrapper.all([compile([CompWithTemplateUrl]), compile([CompWithTemplateUrl])])
.then((humanizedTemplates) => {
expect(humanizedTemplates[0]['commands'][1]['commands']).toEqual(['#text(a)']);
expect(humanizedTemplates[1]['commands'][1]['commands']).toEqual(['#text(a)']);
async.done();
});
xhr.flush();
}));
it('should cache components for sequential requests',
inject([AsyncTestCompleter, XHR], (async, xhr: MockXHR) => {
xhr.expect('package:angular2/test/compiler/compUrl.html', 'a');
compile([CompWithTemplateUrl])
.then((humanizedTemplate0) => {
return compile([CompWithTemplateUrl])
.then((humanizedTemplate1) => {
expect(humanizedTemplate0['commands'][1]['commands'])
.toEqual(['#text(a)']);
expect(humanizedTemplate1['commands'][1]['commands'])
.toEqual(['#text(a)']);
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', 'a');
compile([CompWithTemplateUrl])
.then((humanizedTemplate) => {
compiler.clearCache();
xhr.expect('package:angular2/test/compiler/compUrl.html', 'b');
var result = compile([CompWithTemplateUrl]);
xhr.flush();
return result;
})
.then((humanizedTemplate) => {
expect(humanizedTemplate['commands'][1]['commands']).toEqual(['#text(b)']);
async.done();
});
xhr.flush();
}));
});
describe('compileTemplatesCodeGen', () => {
function normalizeComponent(
component: Type): Promise<NormalizedComponentWithViewDirectives> {
var compAndViewDirMetas = [runtimeMetadataResolver.getMetadata(component)].concat(
runtimeMetadataResolver.getViewDirectivesMetadata(component));
return PromiseWrapper.all(compAndViewDirMetas.map(
meta => compiler.normalizeDirectiveMetadata(meta)))
.then((normalizedCompAndViewDirMetas: CompileDirectiveMetadata[]) =>
new NormalizedComponentWithViewDirectives(
normalizedCompAndViewDirMetas[0],
normalizedCompAndViewDirMetas.slice(1)));
}
function compile(components: Type[]): Promise<any[]> {
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.getMetadata(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.getMetadata(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.getMetadata(CompWithBindingsAndStyles);
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();
});
}));
});
});
}
@Component({
selector: 'comp-a',
host: {'[title]': 'someProp'},
moduleId: THIS_MODULE_ID,
exportAs: 'someExportAs'
})
@View({
template: '<a [href]="someProp"></a>',
styles: ['div {color: red}'],
encapsulation: ViewEncapsulation.None
})
class CompWithBindingsAndStyles {
}
@Component({selector: 'tree', moduleId: THIS_MODULE_ID})
@View({template: '<tree></tree>', directives: [TreeComp], encapsulation: ViewEncapsulation.None})
class TreeComp {
}
@Component({selector: 'comp-wit-dup-tpl', moduleId: THIS_MODULE_ID})
@View({
template: '<tree></tree>',
directives: [TreeComp, TreeComp],
encapsulation: ViewEncapsulation.None
})
class CompWithDupDirectives {
}
@Component({selector: 'comp-url', moduleId: THIS_MODULE_ID})
@View({templateUrl: 'compUrl.html', encapsulation: ViewEncapsulation.None})
class CompWithTemplateUrl {
}
@Component({selector: 'comp-tpl', moduleId: THIS_MODULE_ID})
@View({
template: '<template><a [href]="someProp"></a></template>',
encapsulation: ViewEncapsulation.None
})
class CompWithEmbeddedTemplate {
}
@Directive({selector: 'plain', moduleId: THIS_MODULE_ID})
@View({template: ''})
class NonComponent {
}
function testableTemplateModule(sourceModule: SourceModule,
normComp: CompileDirectiveMetadata): SourceModule {
var resultExpression =
`${THIS_MODULE_REF}humanizeTemplate(Host${normComp.type.name}Template.template)`;
var testableSource = `${sourceModule.sourceWithModuleRefs}
${codeGenValueFn(['_'], resultExpression, '_run')};
${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);
}
// Attention: read by eval!
export function humanizeTemplate(
template: CompiledComponentTemplate,
humanizedTemplates: Map<string, {[key: string]: any}> = null): {[key: string]: any} {
if (isBlank(humanizedTemplates)) {
humanizedTemplates = new Map<string, {[key: string]: any}>();
}
var result = humanizedTemplates.get(template.id);
if (isPresent(result)) {
return result;
}
var commands = [];
result = {
'styles': template.styles,
'commands': commands,
'cd': testChangeDetector(template.changeDetectorFactory)
};
humanizedTemplates.set(template.id, result);
visitAllCommands(new CommandHumanizer(commands, humanizedTemplates), template.commands);
return result;
}
class TestContext implements CompWithBindingsAndStyles, TreeComp, CompWithTemplateUrl,
CompWithEmbeddedTemplate, CompWithDupDirectives {
someProp: string;
}
function testChangeDetector(changeDetectorFactory: Function): string[] {
var ctx = new TestContext();
ctx.someProp = 'someCtxValue';
var dir1 = new TestContext();
dir1.someProp = 'someDirValue';
var dispatcher = new TestDispatcher([dir1], []);
var cd = changeDetectorFactory(dispatcher);
var locals = new Locals(null, MapWrapper.createFromStringMap({'someVar': null}));
cd.hydrate(ctx, locals, dispatcher, new TestPipes());
cd.detectChanges();
return dispatcher.log;
}
class CommandHumanizer implements CommandVisitor {
constructor(private result: any[],
private humanizedTemplates: Map<string, {[key: string]: any}>) {}
visitText(cmd: TextCmd, context: any): any {
this.result.push(`#text(${cmd.value})`);
return null;
}
visitNgContent(cmd: NgContentCmd, context: any): any { return null; }
visitBeginElement(cmd: BeginElementCmd, context: any): any {
this.result.push(`<${cmd.name}>`);
return null;
}
visitEndElement(context: any): any {
this.result.push('</>');
return null;
}
visitBeginComponent(cmd: BeginComponentCmd, context: any): any {
this.result.push(`<${cmd.name}>`);
this.result.push(humanizeTemplate(cmd.templateGetter(), this.humanizedTemplates));
return null;
}
visitEndComponent(context: any): any { return this.visitEndElement(context); }
visitEmbeddedTemplate(cmd: EmbeddedTemplateCmd, context: any): any {
this.result.push(`<template>`);
this.result.push({'cd': testChangeDetector(cmd.changeDetectorFactory)});
this.result.push(`</template>`);
return null;
}
}