From 61129fa12d34ef35996eec85a48295f873b21dde Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Wed, 28 Sep 2016 02:10:02 +0200 Subject: [PATCH] fix(compiler): move detection of unsafe properties for binding to ElementSchemaRegistry (#11378) --- .../src/schema/dom_element_schema_registry.ts | 22 + .../src/schema/element_schema_registry.ts | 2 + .../src/template_parser/template_parser.ts | 22 +- .../dom_element_schema_registry_spec.ts | 41 + .../template_parser/template_parser_spec.ts | 2794 +++++++++-------- .../compiler/testing/schema_registry_mock.ts | 22 +- .../compiler/testing/test_bindings.ts | 2 +- .../test/linker/security_integration_spec.ts | 2 +- 8 files changed, 1512 insertions(+), 1395 deletions(-) diff --git a/modules/@angular/compiler/src/schema/dom_element_schema_registry.ts b/modules/@angular/compiler/src/schema/dom_element_schema_registry.ts index 90f83fee1b..929b1f40aa 100644 --- a/modules/@angular/compiler/src/schema/dom_element_schema_registry.ts +++ b/modules/@angular/compiler/src/schema/dom_element_schema_registry.ts @@ -344,4 +344,26 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry { getMappedPropName(propName: string): string { return _ATTR_TO_PROP[propName] || propName; } getDefaultComponentElementName(): string { return 'ng-component'; } + + validateProperty(name: string): {error: boolean, msg?: string} { + if (name.toLowerCase().startsWith('on')) { + const msg = `Binding to event property '${name}' is disallowed for security reasons, ` + + `please use (${name.slice(2)})=...` + + `\nIf '${name}' is a directive input, make sure the directive is imported by the` + + ` current module.`; + return {error: true, msg: msg}; + } else { + return {error: false}; + } + } + + validateAttribute(name: string): {error: boolean, msg?: string} { + if (name.toLowerCase().startsWith('on')) { + const msg = `Binding to event attribute '${name}' is disallowed for security reasons, ` + + `please use (${name.slice(2)})=...`; + return {error: true, msg: msg}; + } else { + return {error: false}; + } + } } diff --git a/modules/@angular/compiler/src/schema/element_schema_registry.ts b/modules/@angular/compiler/src/schema/element_schema_registry.ts index b16428f9e3..52ed080d96 100644 --- a/modules/@angular/compiler/src/schema/element_schema_registry.ts +++ b/modules/@angular/compiler/src/schema/element_schema_registry.ts @@ -14,4 +14,6 @@ export abstract class ElementSchemaRegistry { abstract securityContext(tagName: string, propName: string): any; abstract getMappedPropName(propName: string): string; abstract getDefaultComponentElementName(): string; + abstract validateProperty(name: string): {error: boolean, msg?: string}; + abstract validateAttribute(name: string): {error: boolean, msg?: string}; } diff --git a/modules/@angular/compiler/src/template_parser/template_parser.ts b/modules/@angular/compiler/src/template_parser/template_parser.ts index 62adb99bcd..1f3c58fee3 100644 --- a/modules/@angular/compiler/src/template_parser/template_parser.ts +++ b/modules/@angular/compiler/src/template_parser/template_parser.ts @@ -927,7 +927,7 @@ class TemplateParseVisitor implements html.Visitor { boundPropertyName = this._schemaRegistry.getMappedPropName(partValue); securityContext = this._schemaRegistry.securityContext(elementName, boundPropertyName); bindingType = PropertyBindingType.Property; - this._assertNoEventBinding(boundPropertyName, sourceSpan, false); + this._validatePropertyOrAttributeName(boundPropertyName, sourceSpan, false); if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName, this._schemas)) { let errorMsg = `Can't bind to '${boundPropertyName}' since it isn't a known property of '${elementName}'.`; @@ -942,7 +942,7 @@ class TemplateParseVisitor implements html.Visitor { } else { if (parts[0] == ATTRIBUTE_PREFIX) { boundPropertyName = parts[1]; - this._assertNoEventBinding(boundPropertyName, sourceSpan, true); + this._validatePropertyOrAttributeName(boundPropertyName, sourceSpan, true); // NB: For security purposes, use the mapped property name, not the attribute name. const mapPropName = this._schemaRegistry.getMappedPropName(boundPropertyName); securityContext = this._schemaRegistry.securityContext(elementName, mapPropName); @@ -975,23 +975,19 @@ class TemplateParseVisitor implements html.Visitor { boundPropertyName, bindingType, securityContext, ast, unit, sourceSpan); } + /** * @param propName the name of the property / attribute * @param sourceSpan * @param isAttr true when binding to an attribute * @private */ - private _assertNoEventBinding(propName: string, sourceSpan: ParseSourceSpan, isAttr: boolean): - void { - if (propName.toLowerCase().startsWith('on')) { - let msg = `Binding to event attribute '${propName}' is disallowed for security reasons, ` + - `please use (${propName.slice(2)})=...`; - if (!isAttr) { - msg += - `\nIf '${propName}' is a directive input, make sure the directive is imported by the` + - ` current module.`; - } - this._reportError(msg, sourceSpan, ParseErrorLevel.FATAL); + private _validatePropertyOrAttributeName( + propName: string, sourceSpan: ParseSourceSpan, isAttr: boolean): void { + const report = isAttr ? this._schemaRegistry.validateAttribute(propName) : + this._schemaRegistry.validateProperty(propName); + if (report.error) { + this._reportError(report.msg, sourceSpan, ParseErrorLevel.FATAL); } } diff --git a/modules/@angular/compiler/test/schema/dom_element_schema_registry_spec.ts b/modules/@angular/compiler/test/schema/dom_element_schema_registry_spec.ts index 4e4ecbce43..6ac675beb1 100644 --- a/modules/@angular/compiler/test/schema/dom_element_schema_registry_spec.ts +++ b/modules/@angular/compiler/test/schema/dom_element_schema_registry_spec.ts @@ -105,6 +105,47 @@ export function main() { expect(registry.getMappedPropName('exotic-unknown')).toEqual('exotic-unknown'); }); + it('should return an error message when asserting event properties', () => { + let report = registry.validateProperty('onClick'); + expect(report.error).toBeTruthy(); + expect(report.msg) + .toEqual( + `Binding to event property 'onClick' is disallowed for security reasons, please use (Click)=... +If 'onClick' is a directive input, make sure the directive is imported by the current module.`); + + report = registry.validateProperty('onAnything'); + expect(report.error).toBeTruthy(); + expect(report.msg) + .toEqual( + `Binding to event property 'onAnything' is disallowed for security reasons, please use (Anything)=... +If 'onAnything' is a directive input, make sure the directive is imported by the current module.`); + }); + + it('should return an error message when asserting event attributes', () => { + let report = registry.validateAttribute('onClick'); + expect(report.error).toBeTruthy(); + expect(report.msg) + .toEqual( + `Binding to event attribute 'onClick' is disallowed for security reasons, please use (Click)=...`); + + report = registry.validateAttribute('onAnything'); + expect(report.error).toBeTruthy(); + expect(report.msg) + .toEqual( + `Binding to event attribute 'onAnything' is disallowed for security reasons, please use (Anything)=...`); + }); + + it('should not return an error message when asserting non-event properties or attributes', + () => { + let report = registry.validateProperty('title'); + expect(report.error).toBeFalsy(); + expect(report.msg).not.toBeDefined(); + + report = registry.validateProperty('exotic-unknown'); + expect(report.error).toBeFalsy(); + expect(report.msg).not.toBeDefined(); + }); + it('should return security contexts for elements', () => { expect(registry.securityContext('iframe', 'srcdoc')).toBe(SecurityContext.HTML); expect(registry.securityContext('p', 'innerHTML')).toBe(SecurityContext.HTML); diff --git a/modules/@angular/compiler/test/template_parser/template_parser_spec.ts b/modules/@angular/compiler/test/template_parser/template_parser_spec.ts index dceedebcd4..c477a4f542 100644 --- a/modules/@angular/compiler/test/template_parser/template_parser_spec.ts +++ b/modules/@angular/compiler/test/template_parser/template_parser_spec.ts @@ -26,7 +26,8 @@ const someModuleUrl = 'package:someModule'; const MOCK_SCHEMA_REGISTRY = [{ provide: ElementSchemaRegistry, useValue: new MockSchemaRegistry( - {'invalidProp': false}, {'mappedAttr': 'mappedProp'}, {'unknown': false, 'un-known': false}), + {'invalidProp': false}, {'mappedAttr': 'mappedProp'}, {'unknown': false, 'un-known': false}, + ['onEvent'], ['onEvent']), }]; export function main() { @@ -141,1494 +142,1529 @@ export function main() { }); }); - 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']]); + describe( + 'TemplateParser', () => { + beforeEach(() => { + TestBed.configureCompiler({providers: [TEST_COMPILER_PROVIDERS, MOCK_SCHEMA_REGISTRY]}); }); - it('should parse elements with attributes', () => { - expect(humanizeTplAst(parse('
', [ - ]))).toEqual([[ElementAst, 'div'], [AttrAst, 'a', 'b']]); - }); - }); + commonBeforeEach(); - it('should parse ngContent', () => { - var parsed = parse('', []); - expect(humanizeTplAst(parsed)).toEqual([[NgContentAst]]); - }); + describe('parse', () => { + describe('nodes without bindings', () => { - it('should parse ngContent regardless the namespace', () => { - var parsed = parse('', []); - expect(humanizeTplAst(parsed)).toEqual([ - [ElementAst, ':svg:svg'], - [NgContentAst], - ]); - }); + it('should parse text nodes', () => { + expect(humanizeTplAst(parse('a', []))).toEqual([[TextAst, 'a']]); + }); - it('should parse bound text nodes', () => { - expect(humanizeTplAst(parse('{{a}}', []))).toEqual([[BoundTextAst, '{{ a }}']]); - }); + it('should parse elements with attributes', () => { + expect(humanizeTplAst(parse('
', [ + ]))).toEqual([[ElementAst, 'div'], [AttrAst, 'a', 'b']]); + }); + }); - 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 %}']]); - })); + it('should parse ngContent', () => { + var parsed = parse('', []); + expect(humanizeTplAst(parsed)).toEqual([[NgContentAst]]); + }); - describe('bound properties', () => { + it('should parse ngContent regardless the namespace', () => { + var parsed = parse('', []); + expect(humanizeTplAst(parsed)).toEqual([ + [ElementAst, ':svg:svg'], + [NgContentAst], + ]); + }); - it('should parse mixed case bound properties', () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Property, 'someProp', 'v', null] - ]); - }); + it('should parse bound text nodes', () => { + expect(humanizeTplAst(parse('{{a}}', []))).toEqual([[BoundTextAst, '{{ a }}']]); + }); - it('should parse dash case bound properties', () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Property, 'some-prop', 'v', null] - ]); - }); + 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 %}']]); + })); - it('should normalize property names via the element schema', () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Property, 'mappedProp', 'v', null] - ]); - }); + describe('bound properties', () => { - it('should parse mixed case bound attributes', () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Attribute, 'someAttr', 'v', null] - ]); - }); + it('should parse mixed case bound properties', () => { + expect(humanizeTplAst(parse('
', []))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Property, 'someProp', '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 dash case bound properties', () => { + expect(humanizeTplAst(parse('
', []))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Property, 'some-prop', 'v', null] + ]); + }); - it('should parse mixed case bound classes', () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Class, 'someClass', '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 styles', () => { - expect(humanizeTplAst(parse('
', []))).toEqual([ - [ElementAst, 'div'], - [BoundElementPropertyAst, PropertyBindingType.Style, 'someStyle', 'v', null] - ]); - }); + it('should parse mixed case bound attributes', () => { + expect(humanizeTplAst(parse('
', []))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Attribute, 'someAttr', '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`); - }); + it('should parse and dash case bound classes', () => { + expect(humanizeTplAst(parse('

', []))).toEqual([ + [ElementAst, 'div'], + [BoundElementPropertyAst, PropertyBindingType.Class, 'some-class', 'v', null] + ]); + }); - describe('errors', () => { - it('should throw error when binding to an unknown property', () => { - expect(() => parse('', [])) - .toThrowError(`Template parse errors: + 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: + 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: + 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 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 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 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 [...] 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([ + 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.Animation, 'someAnimation', - 'value2', null + BoundElementPropertyAst, PropertyBindingType.Property, 'prop', '{{ v }}', 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 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 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'} - }); + 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/); + }); - humanizeTplAst(parse('
', [dirA])); - expect(console.warnings.length).toEqual(0); - }); + 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'} + }); - 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} - }); + humanizeTplAst(parse('
', [dirA])); + expect(console.warnings.length).toEqual(0); + }); - 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 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} + }); - 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}) + 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`); }); - } 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}); - } + 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} + }); - 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) + 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 + ] + ]); + }); }); - } - 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)]})) + 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], + ]); + }); }); - } - beforeEach(() => { nextProviderId = 0; }); + 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 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 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'] + ]); + }); - 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: + 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: + 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: + 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: + 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 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: []}) + 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] + ]); + }); }); - 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}) + describe('explicit templates', () => { + it('should create embedded templates for