From 46674d5fac345eea3b421cbe4bddeebaa37dbce0 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Tue, 24 Apr 2018 14:22:55 -0700 Subject: [PATCH] test(ivy): add html to ivy ast transformer tests (#23546) PR Close #23546 --- .../compiler/src/expression_parser/ast.ts | 7 +- packages/compiler/src/render3/r3_ast.ts | 17 +- .../src/render3/r3_template_transform.ts | 6 +- .../compiler/src/render3/view/template.ts | 14 +- packages/compiler/src/render3/view/util.ts | 3 - .../src/template_parser/binding_parser.ts | 16 +- .../src/template_parser/template_ast.ts | 12 +- packages/compiler/test/BUILD.bazel | 3 +- .../test/expression_parser/parser_spec.ts | 4 +- .../test/expression_parser/utils/BUILD.bazel | 15 + .../expression_parser/{ => utils}/unparser.ts | 4 +- .../{ => utils}/validator.ts | 2 +- packages/compiler/test/render3/BUILD.bazel | 2 + .../render3/r3_template_transform_spec.ts | 486 ++++++++++++++++++ .../template_parser/template_parser_spec.ts | 43 +- 15 files changed, 567 insertions(+), 67 deletions(-) create mode 100644 packages/compiler/test/expression_parser/utils/BUILD.bazel rename packages/compiler/test/expression_parser/{ => utils}/unparser.ts (97%) rename packages/compiler/test/expression_parser/{ => utils}/validator.ts (98%) create mode 100644 packages/compiler/test/render3/r3_template_transform_spec.ts diff --git a/packages/compiler/src/expression_parser/ast.ts b/packages/compiler/src/expression_parser/ast.ts index 08977f7251..c511a4dc23 100644 --- a/packages/compiler/src/expression_parser/ast.ts +++ b/packages/compiler/src/expression_parser/ast.ts @@ -705,7 +705,7 @@ export class ParsedVariable { constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {} } -export const enum BoundElementBindingType { +export const enum BindingType { // A regular binding to a property (e.g. `[property]="expression"`). Property, // A binding to an element attribute (e.g. `[attr.name]="expression"`). @@ -720,7 +720,6 @@ export const enum BoundElementBindingType { export class BoundElementProperty { constructor( - public name: string, public type: BoundElementBindingType, - public securityContext: SecurityContext, public value: AST, public unit: string|null, - public sourceSpan: ParseSourceSpan) {} + public name: string, public type: BindingType, public securityContext: SecurityContext, + public value: AST, public unit: string|null, public sourceSpan: ParseSourceSpan) {} } diff --git a/packages/compiler/src/render3/r3_ast.ts b/packages/compiler/src/render3/r3_ast.ts index 029516eb98..52767919ad 100644 --- a/packages/compiler/src/render3/r3_ast.ts +++ b/packages/compiler/src/render3/r3_ast.ts @@ -7,7 +7,7 @@ */ import {SecurityContext} from '../core'; -import {AST, BoundElementBindingType, BoundElementProperty, ParsedEvent, ParsedEventType} from '../expression_parser/ast'; +import {AST, BindingType, BoundElementProperty, ParsedEvent, ParsedEventType} from '../expression_parser/ast'; import {ParseSourceSpan} from '../parse_util'; export interface Node { @@ -29,14 +29,13 @@ export class TextAttribute implements Node { constructor( public name: string, public value: string, public sourceSpan: ParseSourceSpan, public valueSpan?: ParseSourceSpan) {} - visit(visitor: Visitor): Result { return visitor.visitAttribute(this); } + visit(visitor: Visitor): Result { return visitor.visitTextAttribute(this); } } export class BoundAttribute implements Node { constructor( - public name: string, public type: BoundElementBindingType, - public securityContext: SecurityContext, public value: AST, public unit: string|null, - public sourceSpan: ParseSourceSpan) {} + public name: string, public type: BindingType, public securityContext: SecurityContext, + public value: AST, public unit: string|null, public sourceSpan: ParseSourceSpan) {} static fromBoundElementProperty(prop: BoundElementProperty) { return new BoundAttribute( @@ -106,7 +105,7 @@ export interface Visitor { visitContent(content: Content): Result; visitVariable(variable: Variable): Result; visitReference(reference: Reference): Result; - visitAttribute(attribute: TextAttribute): Result; + visitTextAttribute(attribute: TextAttribute): Result; visitBoundAttribute(attribute: BoundAttribute): Result; visitBoundEvent(attribute: BoundEvent): Result; visitText(text: Text): Result; @@ -119,7 +118,7 @@ export class NullVisitor implements Visitor { visitContent(content: Content): void {} visitVariable(variable: Variable): void {} visitReference(reference: Reference): void {} - visitAttribute(attribute: TextAttribute): void {} + visitTextAttribute(attribute: TextAttribute): void {} visitBoundAttribute(attribute: BoundAttribute): void {} visitBoundEvent(attribute: BoundEvent): void {} visitText(text: Text): void {} @@ -141,7 +140,7 @@ export class RecursiveVisitor implements Visitor { visitContent(content: Content): void {} visitVariable(variable: Variable): void {} visitReference(reference: Reference): void {} - visitAttribute(attribute: TextAttribute): void {} + visitTextAttribute(attribute: TextAttribute): void {} visitBoundAttribute(attribute: BoundAttribute): void {} visitBoundEvent(attribute: BoundEvent): void {} visitText(text: Text): void {} @@ -185,7 +184,7 @@ export class TransformVisitor implements Visitor { visitVariable(variable: Variable): Node { return variable; } visitReference(reference: Reference): Node { return reference; } - visitAttribute(attribute: TextAttribute): Node { return attribute; } + visitTextAttribute(attribute: TextAttribute): Node { return attribute; } visitBoundAttribute(attribute: BoundAttribute): Node { return attribute; } visitBoundEvent(attribute: BoundEvent): Node { return attribute; } visitText(text: Text): Node { return text; } diff --git a/packages/compiler/src/render3/r3_template_transform.ts b/packages/compiler/src/render3/r3_template_transform.ts index b5679db9d0..44a4b92e8b 100644 --- a/packages/compiler/src/render3/r3_template_transform.ts +++ b/packages/compiler/src/render3/r3_template_transform.ts @@ -111,9 +111,12 @@ export class HtmlToTemplateTransform implements html.Visitor { inlineTemplateSourceSpan = attribute.valueSpan || attribute.sourceSpan; + const parsedVariables: ParsedVariable[] = []; this.bindingParser.parseInlineTemplateBinding( templateKey, templateValue, attribute.sourceSpan, templateMatchableAttributes, - templateParsedProperties, templateVariables); + templateParsedProperties, parsedVariables); + templateVariables.push( + ...parsedVariables.map(v => new t.Variable(v.name, v.value, v.sourceSpan))); } else { // Check for variables, events, property bindings, interpolation hasBinding = this.parseAttribute( @@ -272,7 +275,6 @@ export class HtmlToTemplateTransform implements html.Visitor { return hasBinding; } - private parseVariable( identifier: string, value: string, sourceSpan: ParseSourceSpan, variables: t.Variable[]) { if (identifier.indexOf('-') > -1) { diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index a27851d86b..9563936249 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -11,7 +11,7 @@ import {CompileReflector} from '../../compile_reflector'; import {BindingForm, BuiltinFunctionCall, LocalResolver, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter'; import {ConstantPool} from '../../constant_pool'; import * as core from '../../core'; -import {AST, AstMemoryEfficientTransformer, BindingPipe, BoundElementBindingType, FunctionCall, ImplicitReceiver, LiteralArray, LiteralMap, LiteralPrimitive, PropertyRead} from '../../expression_parser/ast'; +import {AST, AstMemoryEfficientTransformer, BindingPipe, BindingType, FunctionCall, ImplicitReceiver, LiteralArray, LiteralMap, LiteralPrimitive, PropertyRead} from '../../expression_parser/ast'; import * as o from '../../output/output_ast'; import {ParseSourceSpan} from '../../parse_util'; import {CssSelector, SelectorMatcher} from '../../selector'; @@ -23,10 +23,10 @@ import {R3QueryMetadata} from './api'; import {CONTEXT_NAME, I18N_ATTR, I18N_ATTR_PREFIX, ID_SEPARATOR, IMPLICIT_REFERENCE, MEANING_SEPARATOR, REFERENCE_PREFIX, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, getQueryPredicate, invalid, mapToExpression, noop, temporaryAllocator, trimTrailingNulls, unsupported} from './util'; const BINDING_INSTRUCTION_MAP: {[type: number]: o.ExternalReference} = { - [BoundElementBindingType.Property]: R3.elementProperty, - [BoundElementBindingType.Attribute]: R3.elementAttribute, - [BoundElementBindingType.Class]: R3.elementClassNamed, - [BoundElementBindingType.Style]: R3.elementStyleNamed, + [BindingType.Property]: R3.elementProperty, + [BindingType.Attribute]: R3.elementAttribute, + [BindingType.Class]: R3.elementClassNamed, + [BindingType.Style]: R3.elementStyleNamed, }; export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver { @@ -331,7 +331,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // Generate element input bindings element.inputs.forEach((input: t.BoundAttribute) => { - if (input.type === BoundElementBindingType.Animation) { + if (input.type === BindingType.Animation) { this._unsupported('animations'); } const convertedBinding = this.convertPropertyBinding(implicit, input.value); @@ -431,7 +431,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // These should be handled in the template or element directly. readonly visitReference = invalid; readonly visitVariable = invalid; - readonly visitAttribute = invalid; + readonly visitTextAttribute = invalid; readonly visitBoundAttribute = invalid; readonly visitBoundEvent = invalid; diff --git a/packages/compiler/src/render3/view/util.ts b/packages/compiler/src/render3/view/util.ts index e96e4ed2cd..20e548515a 100644 --- a/packages/compiler/src/render3/view/util.ts +++ b/packages/compiler/src/render3/view/util.ts @@ -12,12 +12,9 @@ import * as t from '../r3_ast'; import {R3QueryMetadata} from './api'; - /** Name of the temporary to use during data binding */ export const TEMPORARY_NAME = '_t'; - - /** Name of the context parameter passed into a template function */ export const CONTEXT_NAME = 'ctx'; diff --git a/packages/compiler/src/template_parser/binding_parser.ts b/packages/compiler/src/template_parser/binding_parser.ts index bd798b81f2..90b71d08eb 100644 --- a/packages/compiler/src/template_parser/binding_parser.ts +++ b/packages/compiler/src/template_parser/binding_parser.ts @@ -8,7 +8,7 @@ import {CompileDirectiveSummary, CompilePipeSummary} from '../compile_metadata'; import {SecurityContext} from '../core'; -import {ASTWithSource, BindingPipe, BoundElementBindingType, BoundElementProperty, EmptyExpr, ParsedEvent, ParsedEventType, ParsedProperty, ParsedPropertyType, ParsedVariable, ParserError, RecursiveAstVisitor, TemplateBinding} from '../expression_parser/ast'; +import {ASTWithSource, BindingPipe, BindingType, BoundElementProperty, EmptyExpr, ParsedEvent, ParsedEventType, ParsedProperty, ParsedPropertyType, ParsedVariable, ParserError, RecursiveAstVisitor, TemplateBinding} from '../expression_parser/ast'; import {Parser} from '../expression_parser/parser'; import {InterpolationConfig} from '../ml_parser/interpolation_config'; import {mergeNsAndName} from '../ml_parser/tags'; @@ -239,12 +239,12 @@ export class BindingParser { BoundElementProperty { if (boundProp.isAnimation) { return new BoundElementProperty( - boundProp.name, BoundElementBindingType.Animation, SecurityContext.NONE, - boundProp.expression, null, boundProp.sourceSpan); + boundProp.name, BindingType.Animation, SecurityContext.NONE, boundProp.expression, null, + boundProp.sourceSpan); } let unit: string|null = null; - let bindingType: BoundElementBindingType = undefined !; + let bindingType: BindingType = undefined !; let boundPropertyName: string|null = null; const parts = boundProp.name.split(PROPERTY_PARTS_SEPARATOR); let securityContexts: SecurityContext[] = undefined !; @@ -264,15 +264,15 @@ export class BindingParser { boundPropertyName = mergeNsAndName(ns, name); } - bindingType = BoundElementBindingType.Attribute; + bindingType = BindingType.Attribute; } else if (parts[0] == CLASS_PREFIX) { boundPropertyName = parts[1]; - bindingType = BoundElementBindingType.Class; + bindingType = BindingType.Class; securityContexts = [SecurityContext.NONE]; } else if (parts[0] == STYLE_PREFIX) { unit = parts.length > 2 ? parts[2] : null; boundPropertyName = parts[1]; - bindingType = BoundElementBindingType.Style; + bindingType = BindingType.Style; securityContexts = [SecurityContext.STYLE]; } } @@ -282,7 +282,7 @@ export class BindingParser { boundPropertyName = this._schemaRegistry.getMappedPropName(boundProp.name); securityContexts = calcPossibleSecurityContexts( this._schemaRegistry, elementSelector, boundPropertyName, false); - bindingType = BoundElementBindingType.Property; + bindingType = BindingType.Property; this._validatePropertyOrAttributeName(boundPropertyName, boundProp.sourceSpan, false); } diff --git a/packages/compiler/src/template_parser/template_ast.ts b/packages/compiler/src/template_parser/template_ast.ts index 438a8b491a..4f69a54608 100644 --- a/packages/compiler/src/template_parser/template_ast.ts +++ b/packages/compiler/src/template_parser/template_ast.ts @@ -9,7 +9,7 @@ import {AstPath} from '../ast_path'; import {CompileDirectiveSummary, CompileProviderMetadata, CompileTokenMetadata} from '../compile_metadata'; import {SecurityContext} from '../core'; -import {AST, BoundElementBindingType, BoundElementProperty, ParsedEvent, ParsedEventType, ParsedVariable} from '../expression_parser/ast'; +import {AST, BindingType, BoundElementProperty, ParsedEvent, ParsedEventType, ParsedVariable} from '../expression_parser/ast'; import {LifecycleHooks} from '../lifecycle_reflector'; import {ParseSourceSpan} from '../parse_util'; @@ -72,11 +72,11 @@ export enum PropertyBindingType { } const BoundPropertyMapping = { - [BoundElementBindingType.Animation]: PropertyBindingType.Animation, - [BoundElementBindingType.Attribute]: PropertyBindingType.Attribute, - [BoundElementBindingType.Class]: PropertyBindingType.Class, - [BoundElementBindingType.Property]: PropertyBindingType.Property, - [BoundElementBindingType.Style]: PropertyBindingType.Style, + [BindingType.Animation]: PropertyBindingType.Animation, + [BindingType.Attribute]: PropertyBindingType.Attribute, + [BindingType.Class]: PropertyBindingType.Class, + [BindingType.Property]: PropertyBindingType.Property, + [BindingType.Style]: PropertyBindingType.Style, }; /** diff --git a/packages/compiler/test/BUILD.bazel b/packages/compiler/test/BUILD.bazel index b944d7dcc2..e7aaa323cf 100644 --- a/packages/compiler/test/BUILD.bazel +++ b/packages/compiler/test/BUILD.bazel @@ -5,7 +5,6 @@ load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test") NODE_ONLY = [ "**/*_node_only_spec.ts", "aot/**/*.ts", - "render3/**/*.ts", ] UTILS = [ @@ -37,6 +36,7 @@ ts_library( "//packages:types", "//packages/common", "//packages/compiler", + "//packages/compiler/test/expression_parser/utils", "//packages/compiler/testing", "//packages/core", "//packages/core/testing", @@ -58,6 +58,7 @@ ts_library( ":test_utils", "//packages/compiler", "//packages/compiler-cli", + "//packages/compiler/test/expression_parser/utils", "//packages/compiler/testing", "//packages/core", ], diff --git a/packages/compiler/test/expression_parser/parser_spec.ts b/packages/compiler/test/expression_parser/parser_spec.ts index 6090e8a73f..e028ea9c3e 100644 --- a/packages/compiler/test/expression_parser/parser_spec.ts +++ b/packages/compiler/test/expression_parser/parser_spec.ts @@ -12,8 +12,8 @@ import {Parser, SplitInterpolation, TemplateBindingParseResult} from '@angular/c import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {unparse} from './unparser'; -import {validate} from './validator'; +import {unparse} from './utils/unparser'; +import {validate} from './utils/validator'; describe('parser', () => { describe('parseAction', () => { diff --git a/packages/compiler/test/expression_parser/utils/BUILD.bazel b/packages/compiler/test/expression_parser/utils/BUILD.bazel new file mode 100644 index 0000000000..3030462016 --- /dev/null +++ b/packages/compiler/test/expression_parser/utils/BUILD.bazel @@ -0,0 +1,15 @@ +load("//tools:defaults.bzl", "ts_library") + +ts_library( + name = "utils", + testonly = 1, + srcs = glob( + ["*.ts"], + ), + visibility = [ + "//packages/compiler/test:__subpackages__", + ], + deps = [ + "//packages/compiler", + ], +) diff --git a/packages/compiler/test/expression_parser/unparser.ts b/packages/compiler/test/expression_parser/utils/unparser.ts similarity index 97% rename from packages/compiler/test/expression_parser/unparser.ts rename to packages/compiler/test/expression_parser/utils/unparser.ts index 4f9aa33e4e..da27bfcbfb 100644 --- a/packages/compiler/test/expression_parser/unparser.ts +++ b/packages/compiler/test/expression_parser/utils/unparser.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '../../src/expression_parser/ast'; -import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../src/ml_parser/interpolation_config'; +import {AST, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '../../../src/expression_parser/ast'; +import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../src/ml_parser/interpolation_config'; class Unparser implements AstVisitor { private static _quoteRegExp = /"/g; diff --git a/packages/compiler/test/expression_parser/validator.ts b/packages/compiler/test/expression_parser/utils/validator.ts similarity index 98% rename from packages/compiler/test/expression_parser/validator.ts rename to packages/compiler/test/expression_parser/utils/validator.ts index c4fd221712..cfdc851da5 100644 --- a/packages/compiler/test/expression_parser/validator.ts +++ b/packages/compiler/test/expression_parser/utils/validator.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead} from '../../src/expression_parser/ast'; +import {AST, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead} from '../../../src/expression_parser/ast'; import {unparse} from './unparser'; diff --git a/packages/compiler/test/render3/BUILD.bazel b/packages/compiler/test/render3/BUILD.bazel index 5f2f64d936..82c60a34e3 100644 --- a/packages/compiler/test/render3/BUILD.bazel +++ b/packages/compiler/test/render3/BUILD.bazel @@ -11,6 +11,8 @@ ts_library( "//packages:types", "//packages/compiler", "//packages/compiler/test:test_utils", + "//packages/compiler/test/expression_parser/utils", + "//packages/compiler/testing", "//packages/core", ], ) diff --git a/packages/compiler/test/render3/r3_template_transform_spec.ts b/packages/compiler/test/render3/r3_template_transform_spec.ts new file mode 100644 index 0000000000..8880d5b584 --- /dev/null +++ b/packages/compiler/test/render3/r3_template_transform_spec.ts @@ -0,0 +1,486 @@ +/** + * @license + * Copyright Google Inc. 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 {BindingType} from '../../src/expression_parser/ast'; +import {Lexer} from '../../src/expression_parser/lexer'; +import {Parser} from '../../src/expression_parser/parser'; +import {visitAll} from '../../src/ml_parser/ast'; +import {HtmlParser} from '../../src/ml_parser/html_parser'; +import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config'; +import {ParseError} from '../../src/parse_util'; +import * as t from '../../src/render3/r3_ast'; +import {HtmlToTemplateTransform} from '../../src/render3/r3_template_transform'; +import {BindingParser} from '../../src/template_parser/binding_parser'; +import {MockSchemaRegistry} from '../../testing'; +import {unparse} from '../expression_parser/utils/unparser'; + +// Parse an html string to IVY specific info +function parse(html: string) { + const htmlParser = new HtmlParser(); + + const parseResult = htmlParser.parse(html, 'path:://to/template', true); + + if (parseResult.errors.length > 0) { + const msg = parseResult.errors.map(e => e.toString()).join('\n'); + throw new Error(msg); + } + + const htmlNodes = parseResult.rootNodes; + const expressionErrors: ParseError[] = []; + const expressionParser = new Parser(new Lexer()); + const schemaRegistry = new MockSchemaRegistry( + {'invalidProp': false}, {'mappedAttr': 'mappedProp'}, {'unknown': false, 'un-known': false}, + ['onEvent'], ['onEvent']); + const bindingParser = new BindingParser( + expressionParser, DEFAULT_INTERPOLATION_CONFIG, schemaRegistry, null, expressionErrors); + const r3Transform = new HtmlToTemplateTransform(bindingParser); + + const r3Nodes = visitAll(r3Transform, htmlNodes); + + if (r3Transform.errors) { + const msg = r3Transform.errors.map(e => e.toString()).join('\n'); + throw new Error(msg); + } + + return { + nodes: r3Nodes, + hasNgContent: r3Transform.hasNgContent, + ngContentSelectors: r3Transform.ngContentSelectors, + }; +} + +// Transform an IVY AST to a flat list of nodes to ease testing +class R3AstHumanizer implements t.Visitor { + result: any[] = []; + + visitElement(element: t.Element) { + this.result.push(['Element', element.name]); + this.visitAll([ + element.attributes, + element.inputs, + element.outputs, + element.references, + element.children, + ]); + } + + visitTemplate(template: t.Template) { + this.result.push(['Template']); + this.visitAll([ + template.attributes, + template.inputs, + template.references, + template.variables, + template.children, + ]); + } + + visitContent(content: t.Content) { + this.result.push(['Content', content.selectorIndex]); + t.visitAll(this, content.attributes); + } + + visitVariable(variable: t.Variable) { + this.result.push(['Variable', variable.name, variable.value]); + } + + visitReference(reference: t.Reference) { + this.result.push(['Reference', reference.name, reference.value]); + } + + visitTextAttribute(attribute: t.TextAttribute) { + this.result.push(['TextAttribute', attribute.name, attribute.value]); + } + + visitBoundAttribute(attribute: t.BoundAttribute) { + this.result.push([ + 'BoundAttribute', + attribute.type, + attribute.name, + unparse(attribute.value), + ]); + } + + visitBoundEvent(event: t.BoundEvent) { + this.result.push([ + 'BoundEvent', + event.name, + event.target, + unparse(event.handler), + ]); + } + + visitText(text: t.Text) { this.result.push(['Text', text.value]); } + + visitBoundText(text: t.BoundText) { this.result.push(['BoundText', unparse(text.value)]); } + + private visitAll(nodes: t.Node[][]) { nodes.forEach(node => t.visitAll(this, node)); } +} + +function expectFromHtml(html: string) { + const res = parse(html); + return expectFromR3Nodes(res.nodes); +} + +function expectFromR3Nodes(nodes: t.Node[]) { + const humanizer = new R3AstHumanizer(); + t.visitAll(humanizer, nodes); + return expect(humanizer.result); +} + +describe('R3 template transform', () => { + describe('Nodes without binding', () => { + it('should parse text nodes', () => { + expectFromHtml('a').toEqual([ + ['Text', 'a'], + ]); + }); + + it('should parse elements with attributes', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['TextAttribute', 'a', 'b'], + ]); + }); + + it('should parse ngContent', () => { + const res = parse(''); + expect(res.hasNgContent).toEqual(true); + expect(res.ngContentSelectors).toEqual(['a']); + expectFromR3Nodes(res.nodes).toEqual([ + ['Content', 1], + ['TextAttribute', 'select', 'a'], + ]); + }); + + it('should parse ngContent when it contains WS only', () => { + expectFromHtml(' \n ').toEqual([ + ['Content', 1], + ['TextAttribute', 'select', 'a'], + ]); + }); + + it('should parse ngContent regardless the namespace', () => { + expectFromHtml('').toEqual([ + ['Element', ':svg:svg'], + ['Content', 1], + ['TextAttribute', 'select', 'a'], + ]); + }); + }); + + describe('Bound text nodes', () => { + it('should parse bound text nodes', () => { + expectFromHtml('{{a}}').toEqual([ + ['BoundText', '{{ a }}'], + ]); + }); + }); + + describe('Bound attributes', () => { + it('should parse mixed case bound properties', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['BoundAttribute', BindingType.Property, 'someProp', 'v'], + ]); + }); + + it('should parse bound properties via bind- ', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['BoundAttribute', BindingType.Property, 'prop', 'v'], + ]); + }); + + it('should parse bound properties via {{...}}', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['BoundAttribute', BindingType.Property, 'prop', '{{ v }}'], + ]); + }); + + it('should parse dash case bound properties', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['BoundAttribute', BindingType.Property, 'some-prop', 'v'], + ]); + }); + + it('should parse dotted name bound properties', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['BoundAttribute', BindingType.Property, 'd.ot', 'v'], + ]); + }); + + it('should normalize property names via the element schema', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['BoundAttribute', BindingType.Property, 'mappedProp', 'v'], + ]); + }); + + it('should parse mixed case bound attributes', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['BoundAttribute', BindingType.Attribute, 'someAttr', 'v'], + ]); + }); + + it('should parse and dash case bound classes', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['BoundAttribute', BindingType.Class, 'some-class', 'v'], + ]); + }); + + it('should parse mixed case bound classes', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['BoundAttribute', BindingType.Class, 'someClass', 'v'], + ]); + }); + + it('should parse mixed case bound styles', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['BoundAttribute', BindingType.Style, 'someStyle', 'v'], + ]); + }); + }); + + describe('templates', () => { + it('should support * directives', () => { + expectFromHtml('
').toEqual([ + ['Template'], + ['TextAttribute', 'ngIf', ''], + ['Element', 'div'], + ]); + }); + + it('should support ', () => { + expectFromHtml('').toEqual([ + ['Template'], + ]); + }); + + it('should support regardless the namespace', () => { + expectFromHtml('').toEqual([ + ['Element', ':svg:svg'], + ['Template'], + ]); + }); + + it('should support reference via #...', () => { + expectFromHtml('').toEqual([ + ['Template'], + ['Reference', 'a', ''], + ]); + }); + + it('should support reference via ref-...', () => { + expectFromHtml('').toEqual([ + ['Template'], + ['Reference', 'a', ''], + ]); + }); + + it('should parse variables via let-...', () => { + expectFromHtml('').toEqual([ + ['Template'], + ['Variable', 'a', 'b'], + ]); + }); + }); + + describe('inline templates', () => { + it('should parse variables via let ...', () => { + expectFromHtml('
').toEqual([ + ['Template'], + ['TextAttribute', 'ngIf', ''], + ['Variable', 'a', 'b'], + ['Element', 'div'], + ]); + }); + + it('should parse variables via as ...', () => { + expectFromHtml('
').toEqual([ + ['Template'], + ['TextAttribute', 'ngIf', 'expr '], + ['BoundAttribute', BindingType.Property, 'ngIf', 'expr'], + ['Variable', 'local', 'ngIf'], + ['Element', 'div'], + ]); + }); + }); + + describe('events', () => { + it('should parse bound events with a target', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['BoundEvent', 'event', 'window', 'v'], + ]); + }); + + it('should parse event names case sensitive', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['BoundEvent', 'some-event', null, 'v'], + ]); + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['BoundEvent', 'someEvent', null, 'v'], + ]); + }); + + it('should parse bound events via on-', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['BoundEvent', 'event', null, 'v'], + ]); + }); + + it('should parse bound events and properties via [(...)]', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['BoundAttribute', BindingType.Property, 'prop', 'v'], + ['BoundEvent', 'propChange', null, 'v = $event'], + ]); + }); + + it('should parse bound events and properties via bindon-', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['BoundAttribute', BindingType.Property, 'prop', 'v'], + ['BoundEvent', 'propChange', null, 'v = $event'], + ]); + }); + + // TODO(vicb): Should Error + xit('should report an error on empty expression', () => { + expect(() => parse('
')).toThrowError(/Empty expressions are not allowed/); + + expect(() => parse('
')).toThrowError(/Empty expressions are not allowed/); + }); + }); + + describe('references', () => { + it('should parse references via #...', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['Reference', 'a', ''], + ]); + }); + + it('should parse references via ref-', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['Reference', 'a', ''], + ]); + }); + + it('should parse camel case references', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['Reference', 'someA', ''], + ]); + }); + }); + + // TODO(vicb): Add ng-content test + describe('ng-content', () => {}); + + describe('Ignored elements', () => { + it('should ignore a').toEqual([ + ['Text', 'a'], + ]); + }); + + it('should ignore a').toEqual([ + ['Text', 'a'], + ]); + }); + }); + + describe('', () => { + it('should keep elements if they have an absolute url', () => { + expectFromHtml('').toEqual([ + ['Element', 'link'], + ['TextAttribute', 'rel', 'stylesheet'], + ['TextAttribute', 'href', 'http://someurl'], + ]); + expectFromHtml('').toEqual([ + ['Element', 'link'], + ['TextAttribute', 'REL', 'stylesheet'], + ['TextAttribute', 'href', 'http://someurl'], + ]); + }); + + it('should keep elements if they have no uri', () => { + expectFromHtml('').toEqual([ + ['Element', 'link'], + ['TextAttribute', 'rel', 'stylesheet'], + ]); + expectFromHtml('').toEqual([ + ['Element', 'link'], + ['TextAttribute', 'REL', 'stylesheet'], + ]); + }); + + it('should ignore elements if they have a relative uri', () => { + expectFromHtml('').toEqual([]); + expectFromHtml('').toEqual([]); + }); + }); + + describe('ngNonBindable', () => { + it('should ignore bindings on children of elements with ngNonBindable', () => { + expectFromHtml('
{{b}}
').toEqual([ + ['Element', 'div'], + ['TextAttribute', 'ngNonBindable', ''], + ['Text', '{{b}}'], + ]); + }); + + it('should keep nested children of elements with ngNonBindable', () => { + expectFromHtml('
{{b}}
').toEqual([ + ['Element', 'div'], + ['TextAttribute', 'ngNonBindable', ''], + ['Element', 'span'], + ['Text', '{{b}}'], + ]); + }); + + it('should ignore a
').toEqual([ + ['Element', 'div'], + ['TextAttribute', 'ngNonBindable', ''], + ['Text', 'a'], + ]); + }); + + it('should ignore a
').toEqual([ + ['Element', 'div'], + ['TextAttribute', 'ngNonBindable', ''], + ['Text', 'a'], + ]); + }); + + it('should ignore elements inside of elements with ngNonBindable', + () => { + expectFromHtml('
a
').toEqual([ + ['Element', 'div'], + ['TextAttribute', 'ngNonBindable', ''], + ['Text', 'a'], + ]); + }); + }); +}); \ No newline at end of file diff --git a/packages/compiler/test/template_parser/template_parser_spec.ts b/packages/compiler/test/template_parser/template_parser_spec.ts index 527db692ef..fc0834d9d9 100644 --- a/packages/compiler/test/template_parser/template_parser_spec.ts +++ b/packages/compiler/test/template_parser/template_parser_spec.ts @@ -21,7 +21,7 @@ import {Identifiers, createTokenForExternalReference, createTokenForReference} f import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../src/ml_parser/interpolation_config'; import {noUndefined} from '../../src/util'; import {MockSchemaRegistry} from '../../testing'; -import {unparse} from '../expression_parser/unparser'; +import {unparse} from '../expression_parser/utils/unparser'; import {TEST_COMPILER_PROVIDERS} from '../test_bindings'; const someModuleUrl = 'package:someModule'; @@ -139,12 +139,12 @@ class TemplateHumanizer implements TemplateAstVisitor { visitNgContent(ast: NgContentAst, context: any): any { const res = [NgContentAst]; - this.result.push(this._appendContext(ast, res)); + this.result.push(this._appendSourceSpan(ast, res)); return null; } visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { const res = [EmbeddedTemplateAst]; - this.result.push(this._appendContext(ast, res)); + this.result.push(this._appendSourceSpan(ast, res)); templateVisitAll(this, ast.attrs); templateVisitAll(this, ast.outputs); templateVisitAll(this, ast.references); @@ -155,7 +155,7 @@ class TemplateHumanizer implements TemplateAstVisitor { } visitElement(ast: ElementAst, context: any): any { const res = [ElementAst, ast.name]; - this.result.push(this._appendContext(ast, res)); + this.result.push(this._appendSourceSpan(ast, res)); templateVisitAll(this, ast.attrs); templateVisitAll(this, ast.inputs); templateVisitAll(this, ast.outputs); @@ -166,18 +166,18 @@ class TemplateHumanizer implements TemplateAstVisitor { } visitReference(ast: ReferenceAst, context: any): any { const res = [ReferenceAst, ast.name, ast.value]; - this.result.push(this._appendContext(ast, res)); + 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._appendContext(ast, res)); + 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._appendContext(ast, res)); + this.result.push(this._appendSourceSpan(ast, res)); return null; } visitElementProperty(ast: BoundElementPropertyAst, context: any): any { @@ -185,27 +185,27 @@ class TemplateHumanizer implements TemplateAstVisitor { BoundElementPropertyAst, ast.type, ast.name, unparse(ast.value, this.interpolationConfig), ast.unit ]; - this.result.push(this._appendContext(ast, res)); + 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._appendContext(ast, res)); + 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._appendContext(ast, res)); + this.result.push(this._appendSourceSpan(ast, res)); return null; } visitText(ast: TextAst, context: any): any { const res = [TextAst, ast.value]; - this.result.push(this._appendContext(ast, res)); + this.result.push(this._appendSourceSpan(ast, res)); return null; } visitDirective(ast: DirectiveAst, context: any): any { const res = [DirectiveAst, ast.directive]; - this.result.push(this._appendContext(ast, res)); + this.result.push(this._appendSourceSpan(ast, res)); templateVisitAll(this, ast.inputs); templateVisitAll(this, ast.hostProperties); templateVisitAll(this, ast.hostEvents); @@ -215,11 +215,11 @@ class TemplateHumanizer implements TemplateAstVisitor { const res = [ BoundDirectivePropertyAst, ast.directiveName, unparse(ast.value, this.interpolationConfig) ]; - this.result.push(this._appendContext(ast, res)); + this.result.push(this._appendSourceSpan(ast, res)); return null; } - private _appendContext(ast: TemplateAst, input: any[]): any[] { + private _appendSourceSpan(ast: TemplateAst, input: any[]): any[] { if (!this.includeSourceSpan) return input; input.push(ast.sourceSpan !.toString()); return input; @@ -1965,14 +1965,13 @@ Property binding a not used by any directive on an embedded template. Make sure describe('', () => { - it('should keep elements if they have an absolute non package: url', - () => { - expect(humanizeTplAst(parse('a', []))) - .toEqual([ - [ElementAst, 'link'], [AttrAst, 'rel', 'stylesheet'], - [AttrAst, 'href', 'http://someurl'], [TextAst, 'a'] - ]); - }); + it('should keep elements if they have an absolute url', () => { + expect(humanizeTplAst(parse('a', []))) + .toEqual([ + [ElementAst, 'link'], [AttrAst, 'rel', 'stylesheet'], + [AttrAst, 'href', 'http://someurl'], [TextAst, 'a'] + ]); + }); it('should keep elements if they have no uri', () => { expect(humanizeTplAst(parse('a', [