/** * @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 {preserveWhitespacesDefault} from '@angular/compiler'; import {CompileDiDependencyMetadata, CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileTemplateMetadata, CompileTokenMetadata, tokenReference} from '@angular/compiler/src/compile_metadata'; import {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry'; import {ElementSchemaRegistry} from '@angular/compiler/src/schema/element_schema_registry'; import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAstType, ReferenceAst, TemplateAst, TemplateAstVisitor, templateVisitAll, TextAst, VariableAst} from '@angular/compiler/src/template_parser/template_ast'; import {splitClasses, TemplateParser} from '@angular/compiler/src/template_parser/template_parser'; import {SchemaMetadata, SecurityContext} from '@angular/core'; import {Console} from '@angular/core/src/console'; import {inject, TestBed} from '@angular/core/testing'; import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_reflector'; import {createTokenForExternalReference, createTokenForReference, Identifiers} from '../../src/identifiers'; import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../src/ml_parser/interpolation_config'; import {newArray} from '../../src/util'; import {MockSchemaRegistry} from '../../testing'; import {unparse} from '../expression_parser/utils/unparser'; import {TEST_COMPILER_PROVIDERS} from '../test_bindings'; import {compileDirectiveMetadataCreate, compileTemplateMetadata, createTypeMeta} from './util/metadata'; const someModuleUrl = 'package:someModule'; const MOCK_SCHEMA_REGISTRY = [{ provide: ElementSchemaRegistry, useValue: new MockSchemaRegistry( {'invalidProp': false}, {'mappedAttr': 'mappedProp'}, {'unknown': false, 'un-known': false}, ['onEvent'], ['onEvent']), }]; function humanizeTplAst( templateAsts: TemplateAst[], interpolationConfig?: InterpolationConfig): any[] { const humanizer = new TemplateHumanizer(false, interpolationConfig); templateVisitAll(humanizer, templateAsts); return humanizer.result; } function humanizeTplAstSourceSpans( templateAsts: TemplateAst[], interpolationConfig?: InterpolationConfig): any[] { const humanizer = new TemplateHumanizer(true, interpolationConfig); templateVisitAll(humanizer, templateAsts); return humanizer.result; } class TemplateHumanizer implements TemplateAstVisitor { result: any[] = []; constructor( private includeSourceSpan: boolean, private interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {} visitNgContent(ast: NgContentAst, context: any): any { const res = [NgContentAst]; this.result.push(this._appendSourceSpan(ast, res)); return null; } visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { const res = [EmbeddedTemplateAst]; this.result.push(this._appendSourceSpan(ast, res)); templateVisitAll(this, ast.attrs); templateVisitAll(this, ast.outputs); templateVisitAll(this, ast.references); templateVisitAll(this, ast.variables); templateVisitAll(this, ast.directives); templateVisitAll(this, ast.children); return null; } visitElement(ast: ElementAst, context: any): any { const res = [ElementAst, ast.name]; this.result.push(this._appendSourceSpan(ast, res)); templateVisitAll(this, ast.attrs); templateVisitAll(this, ast.inputs); templateVisitAll(this, ast.outputs); templateVisitAll(this, ast.references); templateVisitAll(this, ast.directives); templateVisitAll(this, ast.children); return null; } visitReference(ast: ReferenceAst, context: any): any { const res = [ReferenceAst, ast.name, ast.value]; this.result.push(this._appendSourceSpan(ast, res)); return null; } visitVariable(ast: VariableAst, context: any): any { const res = [VariableAst, ast.name, ast.value]; this.result.push(this._appendSourceSpan(ast, res)); return null; } visitEvent(ast: BoundEventAst, context: any): any { const res = [BoundEventAst, ast.name, ast.target, unparse(ast.handler, this.interpolationConfig)]; this.result.push(this._appendSourceSpan(ast, res)); return null; } visitElementProperty(ast: BoundElementPropertyAst, context: any): any { const res = [ BoundElementPropertyAst, ast.type, ast.name, unparse(ast.value, this.interpolationConfig), ast.unit ]; this.result.push(this._appendSourceSpan(ast, res)); return null; } visitAttr(ast: AttrAst, context: any): any { const res = [AttrAst, ast.name, ast.value]; this.result.push(this._appendSourceSpan(ast, res)); return null; } visitBoundText(ast: BoundTextAst, context: any): any { const res = [BoundTextAst, unparse(ast.value, this.interpolationConfig)]; this.result.push(this._appendSourceSpan(ast, res)); return null; } visitText(ast: TextAst, context: any): any { const res = [TextAst, ast.value]; this.result.push(this._appendSourceSpan(ast, res)); return null; } visitDirective(ast: DirectiveAst, context: any): any { const res = [DirectiveAst, ast.directive]; this.result.push(this._appendSourceSpan(ast, res)); templateVisitAll(this, ast.inputs); templateVisitAll(this, ast.hostProperties); templateVisitAll(this, ast.hostEvents); return null; } visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any { const res = [ BoundDirectivePropertyAst, ast.directiveName, unparse(ast.value, this.interpolationConfig) ]; this.result.push(this._appendSourceSpan(ast, res)); return null; } private _appendSourceSpan(ast: TemplateAst, input: any[]): any[] { if (!this.includeSourceSpan) return input; input.push(ast.sourceSpan.toString()); return input; } } function humanizeContentProjection(templateAsts: TemplateAst[]): any[] { const humanizer = new TemplateContentProjectionHumanizer(); templateVisitAll(humanizer, templateAsts); return humanizer.result; } class TemplateContentProjectionHumanizer implements TemplateAstVisitor { result: any[] = []; visitNgContent(ast: NgContentAst, context: any): any { this.result.push(['ng-content', ast.ngContentIndex]); return null; } visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { this.result.push(['template', ast.ngContentIndex]); templateVisitAll(this, ast.children); return null; } visitElement(ast: ElementAst, context: any): any { this.result.push([ast.name, ast.ngContentIndex]); templateVisitAll(this, ast.children); return null; } visitReference(ast: ReferenceAst, context: any): any { return null; } visitVariable(ast: VariableAst, context: any): any { return null; } visitEvent(ast: BoundEventAst, context: any): any { return null; } visitElementProperty(ast: BoundElementPropertyAst, context: any): any { return null; } visitAttr(ast: AttrAst, context: any): any { return null; } visitBoundText(ast: BoundTextAst, context: any): any { this.result.push([`#text(${unparse(ast.value)})`, ast.ngContentIndex]); return null; } visitText(ast: TextAst, context: any): any { this.result.push([`#text(${ast.value})`, ast.ngContentIndex]); return null; } visitDirective(ast: DirectiveAst, context: any): any { return null; } visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any { return null; } } class ThrowingVisitor implements TemplateAstVisitor { visitNgContent(ast: NgContentAst, context: any): any { throw 'not implemented'; } visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { throw 'not implemented'; } visitElement(ast: ElementAst, context: any): any { throw 'not implemented'; } visitReference(ast: ReferenceAst, context: any): any { throw 'not implemented'; } visitVariable(ast: VariableAst, context: any): any { throw 'not implemented'; } visitEvent(ast: BoundEventAst, context: any): any { throw 'not implemented'; } visitElementProperty(ast: BoundElementPropertyAst, context: any): any { throw 'not implemented'; } visitAttr(ast: AttrAst, context: any): any { throw 'not implemented'; } visitBoundText(ast: BoundTextAst, context: any): any { throw 'not implemented'; } visitText(ast: TextAst, context: any): any { throw 'not implemented'; } visitDirective(ast: DirectiveAst, context: any): any { throw 'not implemented'; } visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any { throw 'not implemented'; } } class FooAstTransformer extends ThrowingVisitor { visitElement(ast: ElementAst, context: any): any { if (ast.name != 'div') return ast; return new ElementAst( 'foo', [], [], [], [], [], [], false, [], [], ast.ngContentIndex, ast.sourceSpan, ast.endSourceSpan); } } class BarAstTransformer extends FooAstTransformer { visitElement(ast: ElementAst, context: any): any { if (ast.name != 'foo') return ast; return new ElementAst( 'bar', [], [], [], [], [], [], false, [], [], ast.ngContentIndex, ast.sourceSpan, ast.endSourceSpan); } } class NullVisitor implements TemplateAstVisitor { visitNgContent(ast: NgContentAst, context: any): any {} visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {} visitElement(ast: ElementAst, context: any): any {} visitReference(ast: ReferenceAst, context: any): any {} visitVariable(ast: VariableAst, context: any): any {} visitEvent(ast: BoundEventAst, context: any): any {} visitElementProperty(ast: BoundElementPropertyAst, context: any): any {} visitAttr(ast: AttrAst, context: any): any {} visitBoundText(ast: BoundTextAst, context: any): any {} visitText(ast: TextAst, context: any): any {} visitDirective(ast: DirectiveAst, context: any): any {} visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any {} } class ArrayConsole implements Console { logs: string[] = []; warnings: string[] = []; log(msg: string) { this.logs.push(msg); } warn(msg: string) { this.warnings.push(msg); } } (function() { let ngIf: CompileDirectiveSummary; let parse: ( template: string, directives: CompileDirectiveSummary[], pipes?: CompilePipeSummary[], schemas?: SchemaMetadata[], preserveWhitespaces?: boolean) => TemplateAst[]; let console: ArrayConsole; function configureCompiler() { console = new ArrayConsole(); beforeEach(() => { TestBed.configureCompiler({ providers: [ {provide: Console, useValue: console}, ], }); }); } function commonBeforeEach() { beforeEach(inject([TemplateParser], (parser: TemplateParser) => { const someAnimation = ['someAnimation']; const someTemplate = compileTemplateMetadata({animations: [someAnimation]}); const component = compileDirectiveMetadataCreate({ isHost: false, selector: 'root', template: someTemplate, type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'Root'}}), isComponent: true }); ngIf = compileDirectiveMetadataCreate({ selector: '[ngIf]', template: someTemplate, type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'NgIf'}}), inputs: ['ngIf'] }).toSummary(); parse = (template: string, directives: CompileDirectiveSummary[], pipes: CompilePipeSummary[]|null = null, schemas: SchemaMetadata[] = [], preserveWhitespaces = true): TemplateAst[] => { if (pipes === null) { pipes = []; } return parser .parse( component, template, directives, pipes, schemas, 'TestComp', preserveWhitespaces) .template; }; })); } describe('TemplateAstVisitor', () => { function expectVisitedNode(visitor: TemplateAstVisitor, node: TemplateAst) { expect(node.visit(visitor, null)).toEqual(node); } it('should visit NgContentAst', () => { expectVisitedNode(new class extends NullVisitor { visitNgContent(ast: NgContentAst, context: any): any { return ast; } }, new NgContentAst(0, 0, null!)); }); it('should visit EmbeddedTemplateAst', () => { expectVisitedNode(new class extends NullVisitor { visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any) { return ast; } }, new EmbeddedTemplateAst([], [], [], [], [], [], false, [], [], 0, null!)); }); it('should visit ElementAst', () => { expectVisitedNode(new class extends NullVisitor { visitElement(ast: ElementAst, context: any) { return ast; } }, new ElementAst('foo', [], [], [], [], [], [], false, [], [], 0, null!, null!)); }); it('should visit RefererenceAst', () => { expectVisitedNode(new class extends NullVisitor { visitReference(ast: ReferenceAst, context: any): any { return ast; } }, new ReferenceAst('foo', null!, null!, null!)); }); it('should visit VariableAst', () => { expectVisitedNode(new class extends NullVisitor { visitVariable(ast: VariableAst, context: any): any { return ast; } }, new VariableAst('foo', 'bar', null!)); }); it('should visit BoundEventAst', () => { expectVisitedNode(new class extends NullVisitor { visitEvent(ast: BoundEventAst, context: any): any { return ast; } }, new BoundEventAst('foo', 'bar', 'goo', null!, null!, null!)); }); it('should visit BoundElementPropertyAst', () => { expectVisitedNode(new class extends NullVisitor { visitElementProperty(ast: BoundElementPropertyAst, context: any): any { return ast; } }, new BoundElementPropertyAst('foo', null!, null!, null!, 'bar', null!)); }); it('should visit AttrAst', () => { expectVisitedNode(new class extends NullVisitor { visitAttr(ast: AttrAst, context: any): any { return ast; } }, new AttrAst('foo', 'bar', null!)); }); it('should visit BoundTextAst', () => { expectVisitedNode(new class extends NullVisitor { visitBoundText(ast: BoundTextAst, context: any): any { return ast; } }, new BoundTextAst(null!, 0, null!)); }); it('should visit TextAst', () => { expectVisitedNode(new class extends NullVisitor { visitText(ast: TextAst, context: any): any { return ast; } }, new TextAst('foo', 0, null!)); }); it('should visit DirectiveAst', () => { expectVisitedNode(new class extends NullVisitor { visitDirective(ast: DirectiveAst, context: any): any { return ast; } }, new DirectiveAst(null!, [], [], [], 0, null!)); }); it('should visit DirectiveAst', () => { expectVisitedNode(new class extends NullVisitor { visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any { return ast; } }, new BoundDirectivePropertyAst('foo', 'bar', null!, null!)); }); it('should skip the typed call of a visitor if visit() returns a truthy value', () => { const visitor = new class extends ThrowingVisitor { visit(ast: TemplateAst, context: any): any { return true; } }; const nodes: TemplateAst[] = [ new NgContentAst(0, 0, null!), new EmbeddedTemplateAst([], [], [], [], [], [], false, [], [], 0, null!), new ElementAst('foo', [], [], [], [], [], [], false, [], [], 0, null!, null!), new ReferenceAst('foo', null!, 'bar', null!), new VariableAst('foo', 'bar', null!), new BoundEventAst('foo', 'bar', 'goo', null!, null!, null!), new BoundElementPropertyAst('foo', null!, null!, null!, 'bar', null!), new AttrAst('foo', 'bar', null!), new BoundTextAst(null!, 0, null!), new TextAst('foo', 0, null!), new DirectiveAst(null!, [], [], [], 0, null!), new BoundDirectivePropertyAst('foo', 'bar', null!, null!) ]; const result = templateVisitAll(visitor, nodes, null); expect(result).toEqual(newArray(nodes.length).fill(true)); }); }); describe('TemplateParser Security', () => { // Semi-integration test to make sure TemplateParser properly sets the security context. // Uses the actual DomElementSchemaRegistry. beforeEach(() => { TestBed.configureCompiler({ providers: [ TEST_COMPILER_PROVIDERS, {provide: ElementSchemaRegistry, useClass: DomElementSchemaRegistry, deps: []} ] }); }); configureCompiler(); commonBeforeEach(); describe('security context', () => { function secContext(tpl: string): SecurityContext { const ast = parse(tpl, []); const propBinding = (ast[0]).inputs[0]; return propBinding.securityContext; } it('should set for properties', () => { expect(secContext('
')).toBe(SecurityContext.NONE); expect(secContext('
')).toBe(SecurityContext.HTML); }); it('should set for property value bindings', () => { expect(secContext('
')).toBe(SecurityContext.HTML); }); it('should set for attributes', () => { expect(secContext('')).toBe(SecurityContext.URL); // NB: attributes below need to change case. expect(secContext('')).toBe(SecurityContext.HTML); expect(secContext('')).toBe(SecurityContext.URL); }); it('should set for style', () => { expect(secContext('')).toBe(SecurityContext.STYLE); }); }); }); describe('TemplateParser', () => { beforeEach(() => { TestBed.configureCompiler({providers: [TEST_COMPILER_PROVIDERS, MOCK_SCHEMA_REGISTRY]}); }); configureCompiler(); commonBeforeEach(); describe('parse', () => { describe('nodes without bindings', () => { it('should parse text nodes', () => { expect(humanizeTplAst(parse('a', []))).toEqual([[TextAst, 'a']]); }); it('should parse elements with attributes', () => { expect(humanizeTplAst(parse('
', []))).toEqual([ [ElementAst, 'div'], [AttrAst, 'a', 'b'] ]); }); }); it('should parse ngContent', () => { const parsed = parse('', []); expect(humanizeTplAst(parsed)).toEqual([[NgContentAst]]); }); it('should parse ngContent when it contains WS only', () => { const parsed = parse(' \n ', []); expect(humanizeTplAst(parsed)).toEqual([[NgContentAst]]); }); it('should parse ngContent regardless the namespace', () => { const parsed = parse('', []); expect(humanizeTplAst(parsed)).toEqual([ [ElementAst, ':svg:svg'], [NgContentAst], ]); }); it('should parse bound text nodes', () => { expect(humanizeTplAst(parse('{{a}}', []))).toEqual([[BoundTextAst, '{{ a }}']]); }); it('should parse with custom interpolation config', inject([TemplateParser], (parser: TemplateParser) => { const component = CompileDirectiveMetadata.create({ selector: 'test', type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'Test'}}), isComponent: true, template: new CompileTemplateMetadata({ interpolation: ['{%', '%}'], isInline: false, animations: [], template: null, templateUrl: null, htmlAst: null, ngContentSelectors: [], externalStylesheets: [], styleUrls: [], styles: [], encapsulation: null, preserveWhitespaces: preserveWhitespacesDefault(null), }), isHost: false, exportAs: null, changeDetection: null, inputs: [], outputs: [], host: {}, providers: [], viewProviders: [], queries: [], guards: {}, viewQueries: [], entryComponents: [], componentViewType: null, rendererType: null, componentFactory: null }); expect(humanizeTplAst( parser.parse(component, '{%a%}', [], [], [], 'TestComp', true).template, {start: '{%', end: '%}'})) .toEqual([[BoundTextAst, '{% a %}']]); })); describe('bound properties', () => { it('should parse mixed case bound properties', () => { expect(humanizeTplAst(parse('
', []))).toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'someProp', 'v', null] ]); }); it('should parse dash case bound properties', () => { expect(humanizeTplAst(parse('
', []))).toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'some-prop', 'v', null] ]); }); it('should parse dotted name bound properties', () => { expect(humanizeTplAst(parse('
', []))).toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'dot.name', 'v', null] ]); }); it('should normalize property names via the element schema', () => { expect(humanizeTplAst(parse('
', []))).toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'mappedProp', 'v', null] ]); }); it('should parse mixed case bound attributes', () => { expect(humanizeTplAst(parse('
', []))).toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Attribute, 'someAttr', 'v', null] ]); }); it('should parse mixed case bound attributes with dot in the attribute name', () => { expect(humanizeTplAst(parse('
', []))).toEqual([ [ElementAst, 'div'], [ BoundElementPropertyAst, PropertyBindingType.Attribute, 'someAttr.someAttrSuffix', 'v', null ] ]); }); it('should parse and dash case bound classes', () => { expect(humanizeTplAst(parse('
', []))).toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Class, 'some-class', 'v', null] ]); }); it('should parse mixed case bound classes', () => { expect(humanizeTplAst(parse('
', []))).toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Class, 'someClass', 'v', null] ]); }); it('should parse mixed case bound styles', () => { expect(humanizeTplAst(parse('
', []))).toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Style, 'someStyle', 'v', null] ]); }); describe('errors', () => { it('should throw error when binding to an unknown property', () => { expect(() => parse('', [])) .toThrowError(`Template parse errors: Can't bind to 'invalidProp' since it isn't a known property of 'my-component'. 1. If 'my-component' is an Angular component and it has 'invalidProp' input, then verify that it is part of this module. 2. If 'my-component' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. 3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. ("][invalidProp]="bar">"): TestComp@0:14`); }); it('should throw error when binding to an unknown property of ng-container', () => { expect(() => parse('', [])) .toThrowError( `Template parse errors: Can't bind to 'invalidProp' since it isn't a known property of 'ng-container'. 1. If 'invalidProp' is an Angular directive, then add 'CommonModule' to the '@NgModule.imports' of this component. 2. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.` + ` ("][invalidProp]="bar">"): TestComp@0:14`); }); it('should throw error when binding to an unknown element w/o bindings', () => { expect(() => parse('', [])).toThrowError(`Template parse errors: 'unknown' is not a known element: 1. If 'unknown' is an Angular component, then verify that it is part of this module. 2. To allow any element add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. ("[ERROR ->]"): TestComp@0:0`); }); it('should throw error when binding to an unknown custom element w/o bindings', () => { expect(() => parse('', [])).toThrowError(`Template parse errors: 'un-known' is not a known element: 1. If 'un-known' is an Angular component, then verify that it is part of this module. 2. If 'un-known' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("[ERROR ->]"): TestComp@0:0`); }); it('should throw error when binding to an invalid property', () => { expect(() => parse('', [])) .toThrowError(`Template parse errors: Binding to property 'onEvent' is disallowed for security reasons ("][onEvent]="bar">"): TestComp@0:14`); }); it('should throw error when binding to an invalid attribute', () => { expect(() => parse('', [])) .toThrowError(`Template parse errors: Binding to attribute 'onEvent' is disallowed for security reasons ("][attr.onEvent]="bar">"): TestComp@0:14`); }); }); it('should parse bound properties via [...] and not report them as attributes', () => { expect(humanizeTplAst(parse('
', []))).toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null] ]); }); it('should parse bound properties via bind- and not report them as attributes', () => { expect(humanizeTplAst(parse('
', []))).toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null] ]); }); it('should report missing property names in bind- syntax', () => { expect(() => parse('
', [])).toThrowError(`Template parse errors: Property name is missing in binding ("
]bind->
"): TestComp@0:5`); }); it('should parse bound properties via {{...}} and not report them as attributes', () => { expect(humanizeTplAst(parse('
', []))).toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', '{{ v }}', null] ]); }); it('should parse bound properties via bind-animate- and not report them as attributes', () => { expect(humanizeTplAst(parse('
', [], [], []))) .toEqual([ [ElementAst, 'div'], [ BoundElementPropertyAst, PropertyBindingType.Animation, 'someAnimation', 'value2', null ] ]); }); it('should throw an error when parsing detects non-bound properties via @ that contain a value', () => { expect(() => { parse('
', [], [], []); }) .toThrowError( /Assigning animation triggers via @prop="exp" attributes with an expression is invalid. Use property bindings \(e.g. \[@prop\]="exp"\) or use an attribute without a value \(e.g. @prop\) instead. \("
\]@someAnimation="value2">"\): TestComp@0:5/); }); it('should report missing animation trigger in @ syntax', () => { expect(() => parse('
', [])).toThrowError(`Template parse errors: Animation trigger is missing ("
]@>
"): TestComp@0:5`); }); it('should not issue a warning when host attributes contain a valid property-bound animation trigger', () => { const animationEntries = ['prop']; const dirA = compileDirectiveMetadataCreate({ selector: 'div', template: compileTemplateMetadata({animations: animationEntries}), type: createTypeMeta({ reference: {filePath: someModuleUrl, name: 'DirA'}, }), host: {'[@prop]': 'expr'} }).toSummary(); humanizeTplAst(parse('
', [dirA])); expect(console.warnings.length).toEqual(0); }); it('should throw descriptive error when a host binding is not a string expression', () => { const dirA = compileDirectiveMetadataCreate({ selector: 'broken', type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), host: {'[class.foo]': null!} }).toSummary(); expect(() => { parse('', [dirA]); }) .toThrowError( `Template parse errors:\nValue of the host property binding "class.foo" needs to be a string representing an expression but got "null" (object) ("[ERROR ->]"): TestComp@0:0, Directive DirA`); }); it('should throw descriptive error when a host event is not a string expression', () => { const dirA = compileDirectiveMetadataCreate({ selector: 'broken', type: createTypeMeta({reference: {filePath: someModuleUrl, name: 'DirA'}}), host: {'(click)': null!} }).toSummary(); expect(() => { parse('', [dirA]); }) .toThrowError( `Template parse errors:\nValue of the host listener "click" needs to be a string representing an expression but got "null" (object) ("[ERROR ->]"): TestComp@0:0, Directive DirA`); }); it('should not issue a warning when an animation property is bound without an expression', () => { humanizeTplAst(parse('
', [], [], [])); expect(console.warnings.length).toEqual(0); }); it('should parse bound properties via [@] and not report them as attributes', () => { expect(humanizeTplAst(parse('
', [], [], []))).toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Animation, 'someAnimation', 'value2', null] ]); }); it('should support * directives', () => { expect(humanizeTplAst(parse('
', [ngIf]))).toEqual([ [EmbeddedTemplateAst], [DirectiveAst, ngIf], [BoundDirectivePropertyAst, 'ngIf', 'null'], [ElementAst, 'div'], ]); }); it('should support ', () => { expect(humanizeTplAst(parse('', []))).toEqual([ [EmbeddedTemplateAst], ]); }); it('should treat