/** * @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 {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 {afterEach, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xit} 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}), }]; export function main() { var ngIf: CompileDirectiveMetadata; var parse: (template: string, directives: CompileDirectiveMetadata[], pipes?: CompilePipeMetadata[]) => TemplateAst[]; var console: ArrayConsole; function commonBeforeEach() { beforeEach(() => { console = new ArrayConsole(); TestBed.configureCompiler({providers: [{provide: Console, useValue: console}]}); }); beforeEach(inject([TemplateParser], (parser: TemplateParser) => { var component = CompileDirectiveMetadata.create({ selector: 'root', type: new CompileTypeMetadata( {moduleUrl: someModuleUrl, name: 'Root', reference: {} as Type}), isComponent: true }); ngIf = CompileDirectiveMetadata.create({ selector: '[ngIf]', 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('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', () => { var parsed = parse('', []); expect(humanizeTplAst(parsed)).toEqual([[NgContentAst]]); }); it('should parse ngContent regardless the namespace', () => { var 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 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 animation properties', () => { expect(humanizeTplAst(parse('
', []))).toEqual([ [ElementAst, 'div'], [ BoundElementPropertyAst, PropertyBindingType.Animation, 'something', '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. \("
\]@something="value2">"\): TestComp@0:5/); }); it('should not issue a warning when host attributes contain a valid property-bound animation trigger', () => { var dirA = CompileDirectiveMetadata.create({ selector: 'div', 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, 'something', '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