/** * @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 {CompileAnimationEntryMetadata, CompileDiDependencyMetadata, CompileDirectiveMetadata, CompilePipeMetadata, CompileProviderMetadata, CompileQueryMetadata, CompileTemplateMetadata, CompileTokenMetadata, CompileTypeMetadata} 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, TextAst, VariableAst, templateVisitAll} from '@angular/compiler/src/template_parser/template_ast'; import {TEMPLATE_TRANSFORMS, TemplateParser, splitClasses} from '@angular/compiler/src/template_parser/template_parser'; import {TEST_COMPILER_PROVIDERS} from '@angular/compiler/testing/test_bindings'; import {SchemaMetadata, SecurityContext, Type} from '@angular/core'; import {Console} from '@angular/core/src/console'; import {TestBed} from '@angular/core/testing'; import {beforeEach, describe, expect, inject, it} from '@angular/core/testing/testing_internal'; import {Identifiers, identifierToken, resolveIdentifierToken} from '../../src/identifiers'; import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../src/ml_parser/interpolation_config'; import {MockSchemaRegistry} from '../../testing/index'; import {unparse} from '../expression_parser/unparser'; const someModuleUrl = 'package:someModule'; const MOCK_SCHEMA_REGISTRY = [{ provide: ElementSchemaRegistry, useValue: new MockSchemaRegistry( {'invalidProp': false}, {'mappedAttr': 'mappedProp'}, {'unknown': false, 'un-known': false}, ['onEvent'], ['onEvent']), }]; export function main() { var ngIf: CompileDirectiveMetadata; var parse: ( template: string, directives: CompileDirectiveMetadata[], pipes?: CompilePipeMetadata[], schemas?: SchemaMetadata[]) => TemplateAst[]; var console: ArrayConsole; function commonBeforeEach() { beforeEach(() => { console = new ArrayConsole(); TestBed.configureCompiler({providers: [{provide: Console, useValue: console}]}); }); beforeEach(inject([TemplateParser], (parser: TemplateParser) => { var someAnimation = new CompileAnimationEntryMetadata('someAnimation', []); var someTemplate = new CompileTemplateMetadata({animations: [someAnimation]}); var component = CompileDirectiveMetadata.create({ selector: 'root', template: someTemplate, type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'Root', reference: {} as Type}), isComponent: true }); ngIf = CompileDirectiveMetadata.create({ selector: '[ngIf]', template: someTemplate, type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'NgIf', reference: {} as Type}), inputs: ['ngIf'] }); parse = (template: string, directives: CompileDirectiveMetadata[], pipes: CompilePipeMetadata[] = null, schemas: SchemaMetadata[] = []): TemplateAst[] => { if (pipes === null) { pipes = []; } return parser.parse(component, template, directives, pipes, schemas, 'TestComp'); }; })); } 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)); }); 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)); }); it('should visit BoundElementPropertyAst', () => { expectVisitedNode( new class extends NullVisitor{ visitElementProperty(ast: BoundElementPropertyAst, context: any): any{return ast;} }, new BoundElementPropertyAst('foo', null, null, false, 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, [], [], [], 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, null), new VariableAst('foo', 'bar', null), new BoundEventAst('foo', 'bar', 'goo', null, null), new BoundElementPropertyAst('foo', null, null, false, null, 'bar', null), new AttrAst('foo', 'bar', null), new BoundTextAst(null, 0, null), new TextAst('foo', 0, null), new DirectiveAst(null, [], [], [], null), new BoundDirectivePropertyAst('foo', 'bar', null, null) ]; const result = templateVisitAll(visitor, nodes, null); expect(result).toEqual(new Array(nodes.length).fill(true)); }); }); describe('TemplateParser template transform', () => { beforeEach(() => { TestBed.configureCompiler({providers: TEST_COMPILER_PROVIDERS}); }); beforeEach(() => { TestBed.configureCompiler({ providers: [{provide: TEMPLATE_TRANSFORMS, useValue: new FooAstTransformer(), multi: true}] }); }); describe('single', () => { commonBeforeEach(); it('should transform TemplateAST', () => { expect(humanizeTplAst(parse('
', []))).toEqual([[ElementAst, 'foo']]); }); }); describe('multiple', () => { beforeEach(() => { TestBed.configureCompiler({ providers: [{provide: TEMPLATE_TRANSFORMS, useValue: new BarAstTransformer(), multi: true}] }); }); commonBeforeEach(); it('should compose transformers', () => { expect(humanizeTplAst(parse('
', []))).toEqual([[ElementAst, 'bar']]); }); }); }); 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} ] }); }); commonBeforeEach(); describe('security context', () => { function secContext(tpl: string): SecurityContext { let ast = parse(tpl, []); let 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]}); }); 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: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'Test', reference: {} as Type}), isComponent: true, template: new CompileTemplateMetadata({interpolation: ['{%', '%}']}) }); expect(humanizeTplAst(parser.parse(component, '{%a%}', [], [], [], 'TestComp'), { 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 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 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] ]); }); it('should report invalid prefixes', () => { expect(() => parse('

', [])) .toThrowError( `Template parse errors:\nInvalid property name 'atTr.foo' ("

][atTr.foo]>"): TestComp@0:3`); expect(() => parse('

', [])) .toThrowError( `Template parse errors:\nInvalid property name 'sTyle.foo' ("

][sTyle.foo]>"): TestComp@0:3`); expect(() => parse('

', [])) .toThrowError( `Template parse errors:\nInvalid property name 'Class.foo' ("

][Class.foo]>"): TestComp@0:3`); expect(() => parse('

', [])) .toThrowError( `Template parse errors:\nInvalid property name 'bar.foo' ("

][bar.foo]>"): TestComp@0:3`); }); 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. ("][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. If 'unknown' 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 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 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 not issue a warning when host attributes contain a valid property-bound animation trigger', () => { const animationEntries = [new CompileAnimationEntryMetadata('prop', [])]; var dirA = CompileDirectiveMetadata.create({ selector: 'div', template: new CompileTemplateMetadata({animations: animationEntries}), type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirA', reference: {} as Type}), host: {'[@prop]': 'expr'} }); humanizeTplAst(parse('
', [dirA])); expect(console.warnings.length).toEqual(0); }); it('should throw descriptive error when a host binding is not a string expression', () => { var dirA = CompileDirectiveMetadata.create({ selector: 'broken', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirA', reference: {} as Type}), host: {'[class.foo]': null} }); 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', () => { var dirA = CompileDirectiveMetadata.create({ selector: 'broken', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirA', reference: {} as Type}), host: {'(click)': null} }); 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 ] ]); }); }); describe('events', () => { it('should parse bound events with a target', () => { expect(humanizeTplAst(parse('
', []))).toEqual([ [ElementAst, 'div'], [BoundEventAst, 'event', 'window', 'v'], ]); }); it('should report an error on empty expression', () => { expect(() => parse('
', [])) .toThrowError(/Empty expressions are not allowed/); expect(() => parse('
', [])) .toThrowError(/Empty expressions are not allowed/); }); it('should parse bound events via (...) and not report them as attributes', () => { expect(humanizeTplAst(parse('
', [ ]))).toEqual([[ElementAst, 'div'], [BoundEventAst, 'event', null, 'v']]); }); it('should parse event names case sensitive', () => { expect(humanizeTplAst(parse('
', [ ]))).toEqual([[ElementAst, 'div'], [BoundEventAst, 'some-event', null, 'v']]); expect(humanizeTplAst(parse('
', [ ]))).toEqual([[ElementAst, 'div'], [BoundEventAst, 'someEvent', null, 'v']]); }); it('should parse bound events via on- and not report them as attributes', () => { expect(humanizeTplAst(parse('
', [ ]))).toEqual([[ElementAst, 'div'], [BoundEventAst, 'event', null, 'v']]); }); it('should allow events on explicit embedded templates that are emitted by a directive', () => { var dirA = CompileDirectiveMetadata.create({ selector: 'template', outputs: ['e'], type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirA', reference: {} as Type}) }); expect(humanizeTplAst(parse('', [dirA]))).toEqual([ [EmbeddedTemplateAst], [BoundEventAst, 'e', null, 'f'], [DirectiveAst, dirA], ]); }); }); describe('bindon', () => { it('should parse bound events and properties via [(...)] and not report them as attributes', () => { expect(humanizeTplAst(parse('
', []))).toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null], [BoundEventAst, 'propChange', null, 'v = $event'] ]); }); it('should parse bound events and properties via bindon- and not report them as attributes', () => { expect(humanizeTplAst(parse('
', []))).toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'prop', 'v', null], [BoundEventAst, 'propChange', null, 'v = $event'] ]); }); }); describe('directives', () => { it('should order directives by the directives array in the View and match them only once', () => { var dirA = CompileDirectiveMetadata.create({ selector: '[a]', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirA', reference: {} as Type}) }); var dirB = CompileDirectiveMetadata.create({ selector: '[b]', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirB', reference: {} as Type}) }); var dirC = CompileDirectiveMetadata.create({ selector: '[c]', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirC', reference: {} as Type}) }); expect(humanizeTplAst(parse('
', [dirA, dirB, dirC]))).toEqual([ [ElementAst, 'div'], [AttrAst, 'a', ''], [AttrAst, 'c', ''], [AttrAst, 'b', ''], [AttrAst, 'a', ''], [AttrAst, 'b', ''], [DirectiveAst, dirA], [DirectiveAst, dirB], [DirectiveAst, dirC] ]); }); it('should locate directives in property bindings', () => { var dirA = CompileDirectiveMetadata.create({ selector: '[a=b]', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirA', reference: {} as Type}) }); var dirB = CompileDirectiveMetadata.create({ selector: '[b]', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirB', reference: {} as Type}) }); expect(humanizeTplAst(parse('
', [dirA, dirB]))).toEqual([ [ElementAst, 'div'], [BoundElementPropertyAst, PropertyBindingType.Property, 'a', 'b', null], [DirectiveAst, dirA] ]); }); it('should locate directives in event bindings', () => { var dirA = CompileDirectiveMetadata.create({ selector: '[a]', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirB', reference: {} as Type}) }); expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ [ElementAst, 'div'], [BoundEventAst, 'a', null, 'b'], [DirectiveAst, dirA] ]); }); it('should parse directive host properties', () => { var dirA = CompileDirectiveMetadata.create({ selector: 'div', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirA', reference: {} as Type}), host: {'[a]': 'expr'} }); expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ [ElementAst, 'div'], [DirectiveAst, dirA], [BoundElementPropertyAst, PropertyBindingType.Property, 'a', 'expr', null] ]); }); it('should parse directive host listeners', () => { var dirA = CompileDirectiveMetadata.create({ selector: 'div', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirA', reference: {} as Type}), host: {'(a)': 'expr'} }); expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ [ElementAst, 'div'], [DirectiveAst, dirA], [BoundEventAst, 'a', null, 'expr'] ]); }); it('should parse directive properties', () => { var dirA = CompileDirectiveMetadata.create({ selector: 'div', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirA', reference: {} as Type}), inputs: ['aProp'] }); expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ [ElementAst, 'div'], [DirectiveAst, dirA], [BoundDirectivePropertyAst, 'aProp', 'expr'] ]); }); it('should parse renamed directive properties', () => { var dirA = CompileDirectiveMetadata.create({ selector: 'div', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirA', reference: {} as Type}), inputs: ['b:a'] }); expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ [ElementAst, 'div'], [DirectiveAst, dirA], [BoundDirectivePropertyAst, 'b', 'expr'] ]); }); it('should parse literal directive properties', () => { var dirA = CompileDirectiveMetadata.create({ selector: 'div', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirA', reference: {} as Type}), inputs: ['a'] }); expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ [ElementAst, 'div'], [AttrAst, 'a', 'literal'], [DirectiveAst, dirA], [BoundDirectivePropertyAst, 'a', '"literal"'] ]); }); it('should favor explicit bound properties over literal properties', () => { var dirA = CompileDirectiveMetadata.create({ selector: 'div', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirA', reference: {} as Type}), inputs: ['a'] }); expect(humanizeTplAst(parse('
', [dirA]))) .toEqual([ [ElementAst, 'div'], [AttrAst, 'a', 'literal'], [DirectiveAst, dirA], [BoundDirectivePropertyAst, 'a', '"literal2"'] ]); }); it('should support optional directive properties', () => { var dirA = CompileDirectiveMetadata.create({ selector: 'div', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirA', reference: {} as Type}), inputs: ['a'] }); expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ [ElementAst, 'div'], [DirectiveAst, dirA] ]); }); }); describe('providers', () => { var nextProviderId: number; function createToken(value: string): CompileTokenMetadata { let token: CompileTokenMetadata; if (value.startsWith('type:')) { const name = value.substring(5); token = new CompileTokenMetadata({ identifier: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name, reference: name as any as Type}) }); } else { token = new CompileTokenMetadata({value: value}); } return token; } function createDep(value: string): CompileDiDependencyMetadata { var isOptional = false; if (value.startsWith('optional:')) { isOptional = true; value = value.substring(9); } var isSelf = false; if (value.startsWith('self:')) { isSelf = true; value = value.substring(5); } var isHost = false; if (value.startsWith('host:')) { isHost = true; value = value.substring(5); } return new CompileDiDependencyMetadata({ token: createToken(value), isOptional: isOptional, isSelf: isSelf, isHost: isHost }); } function createProvider( token: string, {multi = false, deps = []}: {multi?: boolean, deps?: string[]} = {}): CompileProviderMetadata { const name = `provider${nextProviderId++}`; return new CompileProviderMetadata({ token: createToken(token), multi: multi, useClass: new CompileTypeMetadata({name, reference: name as any as Type}), deps: deps.map(createDep) }); } function createDir( selector: string, {providers = null, viewProviders = null, deps = [], queries = []}: { providers?: CompileProviderMetadata[], viewProviders?: CompileProviderMetadata[], deps?: string[], queries?: string[] } = {}): CompileDirectiveMetadata { var isComponent = !selector.startsWith('['); return CompileDirectiveMetadata.create({ selector: selector, type: new CompileTypeMetadata({ moduleUrl: someModuleUrl, name: selector, diDeps: deps.map(createDep), reference: selector as any as Type }), isComponent: isComponent, template: new CompileTemplateMetadata({ngContentSelectors: []}), providers: providers, viewProviders: viewProviders, queries: queries.map( (value) => new CompileQueryMetadata({selectors: [createToken(value)]})) }); } beforeEach(() => { nextProviderId = 0; }); it('should provide a component', () => { var comp = createDir('my-comp'); var elAst: ElementAst = parse('', [comp])[0]; expect(elAst.providers.length).toBe(1); expect(elAst.providers[0].providerType).toBe(ProviderAstType.Component); expect(elAst.providers[0].providers[0].useClass).toBe(comp.type); }); it('should provide a directive', () => { var dirA = createDir('[dirA]'); var elAst: ElementAst = parse('
', [dirA])[0]; expect(elAst.providers.length).toBe(1); expect(elAst.providers[0].providerType).toBe(ProviderAstType.Directive); expect(elAst.providers[0].providers[0].useClass).toBe(dirA.type); }); it('should use the public providers of a directive', () => { var provider = createProvider('service'); var dirA = createDir('[dirA]', {providers: [provider]}); var elAst: ElementAst = parse('
', [dirA])[0]; expect(elAst.providers.length).toBe(2); expect(elAst.providers[1].providerType).toBe(ProviderAstType.PublicService); expect(elAst.providers[1].providers).toEqual([provider]); }); it('should use the private providers of a component', () => { var provider = createProvider('service'); var comp = createDir('my-comp', {viewProviders: [provider]}); var elAst: ElementAst = parse('', [comp])[0]; expect(elAst.providers.length).toBe(2); expect(elAst.providers[1].providerType).toBe(ProviderAstType.PrivateService); expect(elAst.providers[1].providers).toEqual([provider]); }); it('should support multi providers', () => { var provider0 = createProvider('service0', {multi: true}); var provider1 = createProvider('service1', {multi: true}); var provider2 = createProvider('service0', {multi: true}); var dirA = createDir('[dirA]', {providers: [provider0, provider1]}); var dirB = createDir('[dirB]', {providers: [provider2]}); var elAst: ElementAst = parse('
', [dirA, dirB])[0]; expect(elAst.providers.length).toBe(4); expect(elAst.providers[2].providers).toEqual([provider0, provider2]); expect(elAst.providers[3].providers).toEqual([provider1]); }); it('should overwrite non multi providers', () => { var provider1 = createProvider('service0'); var provider2 = createProvider('service1'); var provider3 = createProvider('service0'); var dirA = createDir('[dirA]', {providers: [provider1, provider2]}); var dirB = createDir('[dirB]', {providers: [provider3]}); var elAst: ElementAst = parse('
', [dirA, dirB])[0]; expect(elAst.providers.length).toBe(4); expect(elAst.providers[2].providers).toEqual([provider3]); expect(elAst.providers[3].providers).toEqual([provider2]); }); it('should overwrite component providers by directive providers', () => { var compProvider = createProvider('service0'); var dirProvider = createProvider('service0'); var comp = createDir('my-comp', {providers: [compProvider]}); var dirA = createDir('[dirA]', {providers: [dirProvider]}); var elAst: ElementAst = parse('', [dirA, comp])[0]; expect(elAst.providers.length).toBe(3); expect(elAst.providers[2].providers).toEqual([dirProvider]); }); it('should overwrite view providers by directive providers', () => { var viewProvider = createProvider('service0'); var dirProvider = createProvider('service0'); var comp = createDir('my-comp', {viewProviders: [viewProvider]}); var dirA = createDir('[dirA]', {providers: [dirProvider]}); var elAst: ElementAst = parse('', [dirA, comp])[0]; expect(elAst.providers.length).toBe(3); expect(elAst.providers[2].providers).toEqual([dirProvider]); }); it('should overwrite directives by providers', () => { var dirProvider = createProvider('type:my-comp'); var comp = createDir('my-comp', {providers: [dirProvider]}); var elAst: ElementAst = parse('', [comp])[0]; expect(elAst.providers.length).toBe(1); expect(elAst.providers[0].providers).toEqual([dirProvider]); }); it('if mixing multi and non multi providers', () => { var provider0 = createProvider('service0'); var provider1 = createProvider('service0', {multi: true}); var dirA = createDir('[dirA]', {providers: [provider0]}); var dirB = createDir('[dirB]', {providers: [provider1]}); expect(() => parse('
', [dirA, dirB])) .toThrowError( `Template parse errors:\n` + `Mixing multi and non multi provider is not possible for token service0 ("[ERROR ->]
"): TestComp@0:0`); }); it('should sort providers by their DI order', () => { var provider0 = createProvider('service0', {deps: ['type:[dir2]']}); var provider1 = createProvider('service1'); var dir2 = createDir('[dir2]', {deps: ['service1']}); var comp = createDir('my-comp', {providers: [provider0, provider1]}); var elAst: ElementAst = parse('', [comp, dir2])[0]; expect(elAst.providers.length).toBe(4); expect(elAst.providers[0].providers[0].useClass).toEqual(comp.type); expect(elAst.providers[1].providers).toEqual([provider1]); expect(elAst.providers[2].providers[0].useClass).toEqual(dir2.type); expect(elAst.providers[3].providers).toEqual([provider0]); }); it('should sort directives by their DI order', () => { var dir0 = createDir('[dir0]', {deps: ['type:my-comp']}); var dir1 = createDir('[dir1]', {deps: ['type:[dir0]']}); var dir2 = createDir('[dir2]', {deps: ['type:[dir1]']}); var comp = createDir('my-comp'); var elAst: ElementAst = parse('', [comp, dir2, dir0, dir1])[0]; expect(elAst.providers.length).toBe(4); expect(elAst.directives[0].directive).toBe(comp); expect(elAst.directives[1].directive).toBe(dir0); expect(elAst.directives[2].directive).toBe(dir1); expect(elAst.directives[3].directive).toBe(dir2); }); it('should mark directives and dependencies of directives as eager', () => { var provider0 = createProvider('service0'); var provider1 = createProvider('service1'); var dirA = createDir('[dirA]', {providers: [provider0, provider1], deps: ['service0']}); var elAst: ElementAst = parse('
', [dirA])[0]; expect(elAst.providers.length).toBe(3); expect(elAst.providers[0].providers).toEqual([provider0]); expect(elAst.providers[0].eager).toBe(true); expect(elAst.providers[1].providers[0].useClass).toEqual(dirA.type); expect(elAst.providers[1].eager).toBe(true); expect(elAst.providers[2].providers).toEqual([provider1]); expect(elAst.providers[2].eager).toBe(false); }); it('should mark dependencies on parent elements as eager', () => { var provider0 = createProvider('service0'); var provider1 = createProvider('service1'); var dirA = createDir('[dirA]', {providers: [provider0, provider1]}); var dirB = createDir('[dirB]', {deps: ['service0']}); var elAst: ElementAst = parse('
', [dirA, dirB])[0]; expect(elAst.providers.length).toBe(3); expect(elAst.providers[0].providers[0].useClass).toEqual(dirA.type); expect(elAst.providers[0].eager).toBe(true); expect(elAst.providers[1].providers).toEqual([provider0]); expect(elAst.providers[1].eager).toBe(true); expect(elAst.providers[2].providers).toEqual([provider1]); expect(elAst.providers[2].eager).toBe(false); }); it('should mark queried providers as eager', () => { var provider0 = createProvider('service0'); var provider1 = createProvider('service1'); var dirA = createDir('[dirA]', {providers: [provider0, provider1], queries: ['service0']}); var elAst: ElementAst = parse('
', [dirA])[0]; expect(elAst.providers.length).toBe(3); expect(elAst.providers[0].providers[0].useClass).toEqual(dirA.type); expect(elAst.providers[0].eager).toBe(true); expect(elAst.providers[1].providers).toEqual([provider0]); expect(elAst.providers[1].eager).toBe(true); expect(elAst.providers[2].providers).toEqual([provider1]); expect(elAst.providers[2].eager).toBe(false); }); it('should not mark dependencies accross embedded views as eager', () => { var provider0 = createProvider('service0'); var dirA = createDir('[dirA]', {providers: [provider0]}); var dirB = createDir('[dirB]', {deps: ['service0']}); var elAst: ElementAst = parse('
', [dirA, dirB])[0]; expect(elAst.providers.length).toBe(2); expect(elAst.providers[0].providers[0].useClass).toEqual(dirA.type); expect(elAst.providers[0].eager).toBe(true); expect(elAst.providers[1].providers).toEqual([provider0]); expect(elAst.providers[1].eager).toBe(false); }); it('should report missing @Self() deps as errors', () => { var dirA = createDir('[dirA]', {deps: ['self:provider0']}); expect(() => parse('
', [dirA])) .toThrowError( 'Template parse errors:\nNo provider for provider0 ("[ERROR ->]
"): TestComp@0:0'); }); it('should change missing @Self() that are optional to nulls', () => { var dirA = createDir('[dirA]', {deps: ['optional:self:provider0']}); var elAst: ElementAst = parse('
', [dirA])[0]; expect(elAst.providers[0].providers[0].deps[0].isValue).toBe(true); expect(elAst.providers[0].providers[0].deps[0].value).toBe(null); }); it('should report missing @Host() deps as errors', () => { var dirA = createDir('[dirA]', {deps: ['host:provider0']}); expect(() => parse('
', [dirA])) .toThrowError( 'Template parse errors:\nNo provider for provider0 ("[ERROR ->]
"): TestComp@0:0'); }); it('should change missing @Host() that are optional to nulls', () => { var dirA = createDir('[dirA]', {deps: ['optional:host:provider0']}); var elAst: ElementAst = parse('
', [dirA])[0]; expect(elAst.providers[0].providers[0].deps[0].isValue).toBe(true); expect(elAst.providers[0].providers[0].deps[0].value).toBe(null); }); }); describe('references', () => { it('should parse references via #... and not report them as attributes', () => { expect(humanizeTplAst(parse('
', [ ]))).toEqual([[ElementAst, 'div'], [ReferenceAst, 'a', null]]); }); it('should parse references via ref-... and not report them as attributes', () => { expect(humanizeTplAst(parse('
', [ ]))).toEqual([[ElementAst, 'div'], [ReferenceAst, 'a', null]]); }); it('should parse camel case references', () => { expect(humanizeTplAst(parse('
', [ ]))).toEqual([[ElementAst, 'div'], [ReferenceAst, 'someA', null]]); }); it('should assign references with empty value to the element', () => { expect(humanizeTplAst(parse('
', [ ]))).toEqual([[ElementAst, 'div'], [ReferenceAst, 'a', null]]); }); it('should assign references to directives via exportAs', () => { var dirA = CompileDirectiveMetadata.create({ selector: '[a]', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirA', reference: {} as Type}), exportAs: 'dirA' }); expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ [ElementAst, 'div'], [AttrAst, 'a', ''], [ReferenceAst, 'a', identifierToken(dirA.type)], [DirectiveAst, dirA], ]); }); it('should report references with values that dont match a directive as errors', () => { expect(() => parse('
', [])).toThrowError(`Template parse errors: There is no directive with "exportAs" set to "dirA" ("
]#a="dirA">
"): TestComp@0:5`); }); it('should report invalid reference names', () => { expect(() => parse('
', [])).toThrowError(`Template parse errors: "-" is not allowed in reference names ("
]#a-b>
"): TestComp@0:5`); }); it('should report variables as errors', () => { expect(() => parse('
', [])).toThrowError(`Template parse errors: "let-" is only supported on template elements. ("
]let-a>
"): TestComp@0:5`); }); it('should report duplicate reference names', () => { expect(() => parse('
', [])) .toThrowError(`Template parse errors: Reference "#a" is defined several times ("
]#a>
"): TestComp@0:19`); }); it('should not throw error when there is same reference name in different templates', () => { expect(() => parse('
', [])) .not.toThrowError(); }); it('should assign references with empty value to components', () => { var dirA = CompileDirectiveMetadata.create({ selector: '[a]', isComponent: true, type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirA', reference: {} as Type}), exportAs: 'dirA', template: new CompileTemplateMetadata({ngContentSelectors: []}) }); expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ [ElementAst, 'div'], [AttrAst, 'a', ''], [ReferenceAst, 'a', identifierToken(dirA.type)], [DirectiveAst, dirA], ]); }); it('should not locate directives in references', () => { var dirA = CompileDirectiveMetadata.create({ selector: '[a]', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'DirA', reference: {} as Type}) }); expect(humanizeTplAst(parse('
', [dirA]))).toEqual([ [ElementAst, 'div'], [ReferenceAst, 'a', null] ]); }); }); describe('explicit templates', () => { it('should create embedded templates for