/** * @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 {ParseSourceSpan} from '../../src/parse_util'; import * as t from '../../src/render3/r3_ast'; import {parseR3 as parse} from './view/util'; class R3AstSourceSpans implements t.Visitor { result: any[] = []; visitElement(element: t.Element) { this.result.push([ 'Element', humanizeSpan(element.sourceSpan), humanizeSpan(element.startSourceSpan), humanizeSpan(element.endSourceSpan) ]); this.visitAll([ element.attributes, element.inputs, element.outputs, element.references, element.children, ]); } visitTemplate(template: t.Template) { this.result.push([ 'Template', humanizeSpan(template.sourceSpan), humanizeSpan(template.startSourceSpan), humanizeSpan(template.endSourceSpan) ]); this.visitAll([ template.attributes, template.inputs, template.outputs, template.templateAttrs, template.references, template.variables, template.children, ]); } visitContent(content: t.Content) { this.result.push(['Content', humanizeSpan(content.sourceSpan)]); t.visitAll(this, content.attributes); } visitVariable(variable: t.Variable) { this.result.push([ 'Variable', humanizeSpan(variable.sourceSpan), humanizeSpan(variable.keySpan), humanizeSpan(variable.valueSpan), ]); } visitReference(reference: t.Reference) { this.result.push([ 'Reference', humanizeSpan(reference.sourceSpan), humanizeSpan(reference.keySpan), humanizeSpan(reference.valueSpan) ]); } visitTextAttribute(attribute: t.TextAttribute) { this.result.push([ 'TextAttribute', humanizeSpan(attribute.sourceSpan), humanizeSpan(attribute.keySpan), humanizeSpan(attribute.valueSpan) ]); } visitBoundAttribute(attribute: t.BoundAttribute) { this.result.push([ 'BoundAttribute', humanizeSpan(attribute.sourceSpan), humanizeSpan(attribute.keySpan), humanizeSpan(attribute.valueSpan) ]); } visitBoundEvent(event: t.BoundEvent) { this.result.push([ 'BoundEvent', humanizeSpan(event.sourceSpan), humanizeSpan(event.keySpan), humanizeSpan(event.handlerSpan) ]); } visitText(text: t.Text) { this.result.push(['Text', humanizeSpan(text.sourceSpan)]); } visitBoundText(text: t.BoundText) { this.result.push(['BoundText', humanizeSpan(text.sourceSpan)]); } visitIcu(icu: t.Icu) { this.result.push(['Icu', humanizeSpan(icu.sourceSpan)]); for (const key of Object.keys(icu.vars)) { this.result.push(['Icu:Var', humanizeSpan(icu.vars[key].sourceSpan)]); } for (const key of Object.keys(icu.placeholders)) { this.result.push(['Icu:Placeholder', humanizeSpan(icu.placeholders[key].sourceSpan)]); } } private visitAll(nodes: t.Node[][]) { nodes.forEach(node => t.visitAll(this, node)); } } function humanizeSpan(span: ParseSourceSpan|null|undefined): string { if (span === null || span === undefined) { return ``; } return span.toString(); } function expectFromHtml(html: string) { const res = parse(html); return expectFromR3Nodes(res.nodes); } function expectFromR3Nodes(nodes: t.Node[]) { const humanizer = new R3AstSourceSpans(); t.visitAll(humanizer, nodes); return expect(humanizer.result); } describe('R3 AST source spans', () => { describe('nodes without binding', () => { it('is correct for text nodes', () => { expectFromHtml('a').toEqual([ ['Text', 'a'], ]); }); it('is correct for elements with attributes', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['TextAttribute', 'a="b"', 'a', 'b'], ]); }); it('is correct for elements with attributes without value', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['TextAttribute', 'a', 'a', ''], ]); }); it('is correct for self-closing elements with trailing whitespace', () => { expectFromHtml('\n \n').toEqual([ ['Element', '', '', ''], ['Element', '\n', '', ''], ]); }); }); describe('bound text nodes', () => { it('is correct for bound text nodes', () => { expectFromHtml('{{a}}').toEqual([ ['BoundText', '{{a}}'], ]); }); }); describe('bound attributes', () => { it('is correct for bound properties', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['BoundAttribute', '[someProp]="v"', 'someProp', 'v'], ]); }); it('is correct for bound properties without value', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['BoundAttribute', '[someProp]', 'someProp', ''], ]); }); it('is correct for bound properties via bind- ', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['BoundAttribute', 'bind-prop="v"', 'prop', 'v'], ]); }); it('is correct for bound properties via {{...}}', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['BoundAttribute', 'prop="{{v}}"', 'prop', '{{v}}'], ]); }); it('is correct for bound properties via data-', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['BoundAttribute', 'data-prop="{{v}}"', 'prop', '{{v}}'], ]); }); it('is correct for bound properties via @', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['BoundAttribute', 'bind-@animation="v"', 'animation', 'v'], ]); }); it('is correct for bound properties via animation-', () => { expectFromHtml('
').toEqual([ [ 'Element', '
', '
', '
' ], ['BoundAttribute', 'bind-animate-animationName="v"', 'animationName', 'v'], ]); }); it('is correct for bound properties via @ without value', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['BoundAttribute', '@animation', 'animation', ''], ]); }); }); describe('templates', () => { it('is correct for * directives', () => { expectFromHtml('
').toEqual([ ['Template', '
', '
', '
'], ['TextAttribute', 'ngIf', 'ngIf', ''], ['Element', '
', '
', '
'], ]); }); it('is correct for ', () => { expectFromHtml('').toEqual([ ['Template', '', '', ''], ]); }); it('is correct for reference via #...', () => { expectFromHtml('').toEqual([ ['Template', '', '', ''], ['Reference', '#a', 'a', ''], ]); }); it('is correct for reference with name', () => { expectFromHtml('').toEqual([ [ 'Template', '', '', '' ], ['Reference', '#a="b"', 'a', 'b'], ]); }); it('is correct for reference via ref-...', () => { expectFromHtml('').toEqual([ ['Template', '', '', ''], ['Reference', 'ref-a', 'a', ''], ]); }); it('is correct for reference via data-ref-...', () => { expectFromHtml('').toEqual([ [ 'Template', '', '', '' ], ['Reference', 'data-ref-a', 'a', ''], ]); }); it('is correct for variables via let-...', () => { expectFromHtml('').toEqual([ [ 'Template', '', '', '' ], ['Variable', 'let-a="b"', 'a', 'b'], ]); }); it('is correct for variables via data-let-...', () => { expectFromHtml('').toEqual([ [ 'Template', '', '', '' ], ['Variable', 'data-let-a="b"', 'a', 'b'], ]); }); it('is correct for attributes', () => { expectFromHtml('').toEqual([ [ 'Template', '', '', '' ], ['TextAttribute', 'k1="v1"', 'k1', 'v1'], ]); }); it('is correct for bound attributes', () => { expectFromHtml('').toEqual([ [ 'Template', '', '', '' ], ['BoundAttribute', '[k1]="v1"', 'k1', 'v1'], ]); }); }); // TODO(joost): improve spans of nodes extracted from macrosyntax describe('inline templates', () => { it('is correct for attribute and bound attributes', () => { // Desugared form is // //
//
expectFromHtml('
').toEqual([ [ 'Template', '
', '
', '
' ], ['TextAttribute', 'ngFor', 'ngFor', ''], ['BoundAttribute', 'of items', 'of', 'items'], ['Variable', 'let item ', 'item', ''], [ 'Element', '
', '
', '
' ], ]); // Note that this test exercises an *incorrect* usage of the ngFor // directive. There is a missing 'let' in the beginning of the expression // which causes the template to be desugared into // //
//
expectFromHtml('
').toEqual([ [ 'Template', '
', '
', '
' ], ['BoundAttribute', 'ngFor="item ', 'ngFor', 'item'], ['BoundAttribute', 'of items', 'of', 'items'], ['Element', '
', '
', '
'], ]); expectFromHtml('
').toEqual([ [ 'Template', '
', '
', '
' ], ['TextAttribute', 'ngFor', 'ngFor', ''], ['BoundAttribute', 'of items; ', 'of', 'items'], ['BoundAttribute', 'trackBy: trackByFn', 'trackBy', 'trackByFn'], ['Variable', 'let item ', 'item', ''], [ 'Element', '
', '
', '
' ], ]); }); it('is correct for variables via let ...', () => { expectFromHtml('
').toEqual([ ['Template', '
', '
', '
'], ['TextAttribute', 'ngIf', 'ngIf', ''], ['Variable', 'let a=b', 'a', 'b'], ['Element', '
', '
', '
'], ]); }); it('is correct for variables via as ...', () => { expectFromHtml('
').toEqual([ ['Template', '
', '
', '
'], ['BoundAttribute', 'ngIf="expr ', 'ngIf', 'expr'], ['Variable', 'ngIf="expr as local', 'local', 'ngIf'], ['Element', '
', '
', '
'], ]); }); }); describe('events', () => { it('is correct for event names case sensitive', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['BoundEvent', '(someEvent)="v"', 'someEvent', 'v'], ]); }); it('is correct for bound events via on-', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['BoundEvent', 'on-event="v"', 'event', 'v'], ]); }); it('is correct for bound events via data-on-', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['BoundEvent', 'data-on-event="v"', 'event', 'v'], ]); }); it('is correct for bound events and properties via [(...)]', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['BoundAttribute', '[(prop)]="v"', 'prop', 'v'], ['BoundEvent', '[(prop)]="v"', 'prop', 'v'], ]); }); it('is correct for bound events and properties via bindon-', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['BoundAttribute', 'bindon-prop="v"', 'prop', 'v'], ['BoundEvent', 'bindon-prop="v"', 'prop', 'v'], ]); }); it('is correct for bound events and properties via data-bindon-', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['BoundAttribute', 'data-bindon-prop="v"', 'prop', 'v'], ['BoundEvent', 'data-bindon-prop="v"', 'prop', 'v'], ]); }); it('is correct for bound events via @', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['BoundEvent', '(@name.done)="v"', 'name.done', 'v'], ]); }); }); describe('references', () => { it('is correct for references via #...', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['Reference', '#a', 'a', ''], ]); }); it('is correct for references with name', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['Reference', '#a="b"', 'a', 'b'], ]); }); it('is correct for references via ref-', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['Reference', 'ref-a', 'a', ''], ]); }); it('is correct for references via data-ref-', () => { expectFromHtml('
').toEqual([ ['Element', '
', '
', '
'], ['Reference', 'ref-a', 'a', ''], ]); }); }); describe('ICU expressions', () => { it('is correct for variables and placeholders', () => { expectFromHtml('{item.var, plural, other { {{item.placeholder}} items } }') .toEqual([ [ 'Element', '{item.var, plural, other { {{item.placeholder}} items } }', '', '' ], ['Icu', '{item.var, plural, other { {{item.placeholder}} items } }'], ['Icu:Var', 'item.var'], ['Icu:Placeholder', '{{item.placeholder}}'], ]); }); it('is correct for nested ICUs', () => { expectFromHtml( '{item.var, plural, other { {{item.placeholder}} {nestedVar, plural, other { {{nestedPlaceholder}} }}} }') .toEqual([ [ 'Element', '{item.var, plural, other { {{item.placeholder}} {nestedVar, plural, other { {{nestedPlaceholder}} }}} }', '', '' ], [ 'Icu', '{item.var, plural, other { {{item.placeholder}} {nestedVar, plural, other { {{nestedPlaceholder}} }}} }' ], ['Icu:Var', 'nestedVar'], ['Icu:Var', 'item.var'], ['Icu:Placeholder', '{{item.placeholder}}'], ['Icu:Placeholder', '{{nestedPlaceholder}}'], ]); }); }); });