| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @license | 
					
						
							| 
									
										
										
										
											2020-05-19 12:08:49 -07:00
										 |  |  |  * Copyright Google LLC All Rights Reserved. | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |  * | 
					
						
							|  |  |  |  * Use of this source code is governed by an MIT-style license that can be | 
					
						
							|  |  |  |  * found in the LICENSE file at https://angular.io/license
 | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import {BindingType} from '../../src/expression_parser/ast'; | 
					
						
							|  |  |  | import * as t from '../../src/render3/r3_ast'; | 
					
						
							|  |  |  | import {unparse} from '../expression_parser/utils/unparser'; | 
					
						
							| 
									
										
										
										
											2018-10-18 10:08:51 -07:00
										 |  |  | import {parseR3 as parse} from './view/util'; | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-27 14:39:07 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  | // Transform an IVY AST to a flat list of nodes to ease testing
 | 
					
						
							|  |  |  | class R3AstHumanizer implements t.Visitor<void> { | 
					
						
							|  |  |  |   result: any[] = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   visitElement(element: t.Element) { | 
					
						
							|  |  |  |     this.result.push(['Element', element.name]); | 
					
						
							|  |  |  |     this.visitAll([ | 
					
						
							|  |  |  |       element.attributes, | 
					
						
							|  |  |  |       element.inputs, | 
					
						
							|  |  |  |       element.outputs, | 
					
						
							|  |  |  |       element.references, | 
					
						
							|  |  |  |       element.children, | 
					
						
							|  |  |  |     ]); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   visitTemplate(template: t.Template) { | 
					
						
							|  |  |  |     this.result.push(['Template']); | 
					
						
							|  |  |  |     this.visitAll([ | 
					
						
							|  |  |  |       template.attributes, | 
					
						
							|  |  |  |       template.inputs, | 
					
						
							| 
									
										
										
										
											2019-03-07 08:31:31 +00:00
										 |  |  |       template.outputs, | 
					
						
							|  |  |  |       template.templateAttrs, | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |       template.references, | 
					
						
							|  |  |  |       template.variables, | 
					
						
							|  |  |  |       template.children, | 
					
						
							|  |  |  |     ]); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   visitContent(content: t.Content) { | 
					
						
							| 
									
										
										
										
											2018-11-30 15:01:37 -08:00
										 |  |  |     this.result.push(['Content', content.selector]); | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |     t.visitAll(this, content.attributes); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   visitVariable(variable: t.Variable) { | 
					
						
							|  |  |  |     this.result.push(['Variable', variable.name, variable.value]); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   visitReference(reference: t.Reference) { | 
					
						
							|  |  |  |     this.result.push(['Reference', reference.name, reference.value]); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   visitTextAttribute(attribute: t.TextAttribute) { | 
					
						
							|  |  |  |     this.result.push(['TextAttribute', attribute.name, attribute.value]); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   visitBoundAttribute(attribute: t.BoundAttribute) { | 
					
						
							|  |  |  |     this.result.push([ | 
					
						
							|  |  |  |       'BoundAttribute', | 
					
						
							|  |  |  |       attribute.type, | 
					
						
							|  |  |  |       attribute.name, | 
					
						
							|  |  |  |       unparse(attribute.value), | 
					
						
							|  |  |  |     ]); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   visitBoundEvent(event: t.BoundEvent) { | 
					
						
							|  |  |  |     this.result.push([ | 
					
						
							|  |  |  |       'BoundEvent', | 
					
						
							|  |  |  |       event.name, | 
					
						
							|  |  |  |       event.target, | 
					
						
							|  |  |  |       unparse(event.handler), | 
					
						
							|  |  |  |     ]); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-08 10:14:18 -07:00
										 |  |  |   visitText(text: t.Text) { | 
					
						
							|  |  |  |     this.result.push(['Text', text.value]); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-08 10:14:18 -07:00
										 |  |  |   visitBoundText(text: t.BoundText) { | 
					
						
							|  |  |  |     this.result.push(['BoundText', unparse(text.value)]); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-08 10:14:18 -07:00
										 |  |  |   visitIcu(icu: t.Icu) { | 
					
						
							|  |  |  |     return null; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2018-10-18 10:08:51 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-08 10:14:18 -07:00
										 |  |  |   private visitAll(nodes: t.Node[][]) { | 
					
						
							|  |  |  |     nodes.forEach(node => t.visitAll(this, node)); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-02 11:14:07 +08:00
										 |  |  | function expectFromHtml(html: string, ignoreError = false) { | 
					
						
							|  |  |  |   const res = parse(html, {ignoreError}); | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |   return expectFromR3Nodes(res.nodes); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function expectFromR3Nodes(nodes: t.Node[]) { | 
					
						
							|  |  |  |   const humanizer = new R3AstHumanizer(); | 
					
						
							|  |  |  |   t.visitAll(humanizer, nodes); | 
					
						
							|  |  |  |   return expect(humanizer.result); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-06-20 18:42:44 -07:00
										 |  |  | function expectSpanFromHtml(html: string) { | 
					
						
							|  |  |  |   const {nodes} = parse(html); | 
					
						
							| 
									
										
										
										
											2020-04-08 10:14:18 -07:00
										 |  |  |   return expect(nodes[0]!.sourceSpan.toString()); | 
					
						
							| 
									
										
										
										
											2019-06-20 18:42:44 -07:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  | describe('R3 template transform', () => { | 
					
						
							| 
									
										
										
										
											2019-06-20 18:42:44 -07:00
										 |  |  |   describe('ParseSpan on nodes toString', () => { | 
					
						
							| 
									
										
										
										
											2020-04-08 10:14:18 -07:00
										 |  |  |     it('should create valid text span on Element with adjacent start and end tags', () => { | 
					
						
							|  |  |  |       expectSpanFromHtml('<div></div>').toBe('<div></div>'); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2019-06-20 18:42:44 -07:00
										 |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |   describe('Nodes without binding', () => { | 
					
						
							| 
									
										
										
										
											2021-03-02 13:00:45 -08:00
										 |  |  |     it('should parse incomplete tags terminated by EOF', () => { | 
					
						
							|  |  |  |       expectFromHtml('<a', true /* ignoreError */).toEqual([ | 
					
						
							|  |  |  |         ['Element', 'a'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse incomplete tags terminated by another tag', () => { | 
					
						
							|  |  |  |       expectFromHtml('<a <span></span>', true /* ignoreError */).toEqual([ | 
					
						
							|  |  |  |         ['Element', 'a'], | 
					
						
							|  |  |  |         ['Element', 'span'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |     it('should parse text nodes', () => { | 
					
						
							|  |  |  |       expectFromHtml('a').toEqual([ | 
					
						
							|  |  |  |         ['Text', 'a'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse elements with attributes', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div a=b></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['TextAttribute', 'a', 'b'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse ngContent', () => { | 
					
						
							|  |  |  |       const res = parse('<ng-content select="a"></ng-content>'); | 
					
						
							|  |  |  |       expectFromR3Nodes(res.nodes).toEqual([ | 
					
						
							| 
									
										
										
										
											2018-11-30 15:01:37 -08:00
										 |  |  |         ['Content', 'a'], | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |         ['TextAttribute', 'select', 'a'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse ngContent when it contains WS only', () => { | 
					
						
							|  |  |  |       expectFromHtml('<ng-content select="a">    \n   </ng-content>').toEqual([ | 
					
						
							| 
									
										
										
										
											2018-11-30 15:01:37 -08:00
										 |  |  |         ['Content', 'a'], | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |         ['TextAttribute', 'select', 'a'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse ngContent regardless the namespace', () => { | 
					
						
							|  |  |  |       expectFromHtml('<svg><ng-content select="a"></ng-content></svg>').toEqual([ | 
					
						
							|  |  |  |         ['Element', ':svg:svg'], | 
					
						
							| 
									
										
										
										
											2018-11-30 15:01:37 -08:00
										 |  |  |         ['Content', 'a'], | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |         ['TextAttribute', 'select', 'a'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('Bound text nodes', () => { | 
					
						
							|  |  |  |     it('should parse bound text nodes', () => { | 
					
						
							|  |  |  |       expectFromHtml('{{a}}').toEqual([ | 
					
						
							|  |  |  |         ['BoundText', '{{ a }}'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('Bound attributes', () => { | 
					
						
							|  |  |  |     it('should parse mixed case bound properties', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div [someProp]="v"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['BoundAttribute', BindingType.Property, 'someProp', 'v'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse bound properties via bind- ', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div bind-prop="v"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['BoundAttribute', BindingType.Property, 'prop', 'v'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-28 18:02:09 -06:00
										 |  |  |     it('should report missing property names in bind- syntax', () => { | 
					
						
							|  |  |  |       expect(() => parse('<div bind-></div>')).toThrowError(/Property name is missing in binding/); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |     it('should parse bound properties via {{...}}', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div prop="{{v}}"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['BoundAttribute', BindingType.Property, 'prop', '{{ v }}'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse dash case bound properties', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div [some-prop]="v"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['BoundAttribute', BindingType.Property, 'some-prop', 'v'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse dotted name bound properties', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div [d.ot]="v"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['BoundAttribute', BindingType.Property, 'd.ot', 'v'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-02-15 21:55:07 +01:00
										 |  |  |     it('should not normalize property names via the element schema', () => { | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |       expectFromHtml('<div [mappedAttr]="v"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							| 
									
										
										
										
											2019-02-15 21:55:07 +01:00
										 |  |  |         ['BoundAttribute', BindingType.Property, 'mappedAttr', 'v'], | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse mixed case bound attributes', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div [attr.someAttr]="v"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['BoundAttribute', BindingType.Attribute, 'someAttr', 'v'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse and dash case bound classes', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div [class.some-class]="v"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['BoundAttribute', BindingType.Class, 'some-class', 'v'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse mixed case bound classes', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div [class.someClass]="v"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['BoundAttribute', BindingType.Class, 'someClass', 'v'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse mixed case bound styles', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div [style.someStyle]="v"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['BoundAttribute', BindingType.Style, 'someStyle', 'v'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('templates', () => { | 
					
						
							|  |  |  |     it('should support * directives', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div *ngIf></div>').toEqual([ | 
					
						
							|  |  |  |         ['Template'], | 
					
						
							|  |  |  |         ['TextAttribute', 'ngIf', ''], | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should support <ng-template>', () => { | 
					
						
							|  |  |  |       expectFromHtml('<ng-template></ng-template>').toEqual([ | 
					
						
							|  |  |  |         ['Template'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should support <ng-template> regardless the namespace', () => { | 
					
						
							|  |  |  |       expectFromHtml('<svg><ng-template></ng-template></svg>').toEqual([ | 
					
						
							|  |  |  |         ['Element', ':svg:svg'], | 
					
						
							|  |  |  |         ['Template'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should support reference via #...', () => { | 
					
						
							|  |  |  |       expectFromHtml('<ng-template #a></ng-template>').toEqual([ | 
					
						
							|  |  |  |         ['Template'], | 
					
						
							|  |  |  |         ['Reference', 'a', ''], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should support reference via ref-...', () => { | 
					
						
							|  |  |  |       expectFromHtml('<ng-template ref-a></ng-template>').toEqual([ | 
					
						
							|  |  |  |         ['Template'], | 
					
						
							|  |  |  |         ['Reference', 'a', ''], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-23 22:10:12 +01:00
										 |  |  |     it('should report an error if a reference is used multiple times on the same template', () => { | 
					
						
							|  |  |  |       expect(() => parse('<ng-template #a #a></ng-template>')) | 
					
						
							|  |  |  |           .toThrowError(/Reference "#a" is defined more than once/); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |     it('should parse variables via let-...', () => { | 
					
						
							|  |  |  |       expectFromHtml('<ng-template let-a="b"></ng-template>').toEqual([ | 
					
						
							|  |  |  |         ['Template'], | 
					
						
							|  |  |  |         ['Variable', 'a', 'b'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2018-08-01 21:39:31 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     it('should parse attributes', () => { | 
					
						
							|  |  |  |       expectFromHtml('<ng-template k1="v1" k2="v2"></ng-template>').toEqual([ | 
					
						
							|  |  |  |         ['Template'], | 
					
						
							|  |  |  |         ['TextAttribute', 'k1', 'v1'], | 
					
						
							|  |  |  |         ['TextAttribute', 'k2', 'v2'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse bound attributes', () => { | 
					
						
							|  |  |  |       expectFromHtml('<ng-template [k1]="v1" [k2]="v2"></ng-template>').toEqual([ | 
					
						
							|  |  |  |         ['Template'], | 
					
						
							|  |  |  |         ['BoundAttribute', BindingType.Property, 'k1', 'v1'], | 
					
						
							|  |  |  |         ['BoundAttribute', BindingType.Property, 'k2', 'v2'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('inline templates', () => { | 
					
						
							| 
									
										
										
										
											2018-08-02 11:32:04 -07:00
										 |  |  |     it('should support attribute and bound attributes', () => { | 
					
						
							| 
									
										
										
										
											2020-02-25 12:23:03 -08:00
										 |  |  |       // Desugared form is
 | 
					
						
							|  |  |  |       // <ng-template ngFor [ngForOf]="items" let-item>
 | 
					
						
							|  |  |  |       //   <div></div>
 | 
					
						
							|  |  |  |       // </ng-template>
 | 
					
						
							|  |  |  |       expectFromHtml('<div *ngFor="let item of items"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Template'], | 
					
						
							|  |  |  |         ['TextAttribute', 'ngFor', ''], | 
					
						
							|  |  |  |         ['BoundAttribute', BindingType.Property, 'ngForOf', 'items'], | 
					
						
							|  |  |  |         ['Variable', 'item', '$implicit'], | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Note that this test exercises an *incorrect* usage of the ngFor
 | 
					
						
							|  |  |  |       // directive. There is a missing 'let' in the beginning of the expression
 | 
					
						
							|  |  |  |       // which causes the template to be desugared into
 | 
					
						
							|  |  |  |       // <ng-template [ngFor]="item" [ngForOf]="items">
 | 
					
						
							|  |  |  |       //   <div></div>
 | 
					
						
							|  |  |  |       // </ng-template>
 | 
					
						
							| 
									
										
										
										
											2018-08-02 11:32:04 -07:00
										 |  |  |       expectFromHtml('<div *ngFor="item of items"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Template'], | 
					
						
							|  |  |  |         ['BoundAttribute', BindingType.Property, 'ngFor', 'item'], | 
					
						
							|  |  |  |         ['BoundAttribute', BindingType.Property, 'ngForOf', 'items'], | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |     it('should parse variables via let ...', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div *ngIf="let a=b"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Template'], | 
					
						
							|  |  |  |         ['TextAttribute', 'ngIf', ''], | 
					
						
							|  |  |  |         ['Variable', 'a', 'b'], | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse variables via as ...', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div *ngIf="expr as local"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Template'], | 
					
						
							|  |  |  |         ['BoundAttribute', BindingType.Property, 'ngIf', 'expr'], | 
					
						
							|  |  |  |         ['Variable', 'local', 'ngIf'], | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('events', () => { | 
					
						
							|  |  |  |     it('should parse bound events with a target', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div (window:event)="v"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['BoundEvent', 'event', 'window', 'v'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse event names case sensitive', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div (some-event)="v"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['BoundEvent', 'some-event', null, 'v'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |       expectFromHtml('<div (someEvent)="v"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['BoundEvent', 'someEvent', null, 'v'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse bound events via on-', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div on-event="v"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['BoundEvent', 'event', null, 'v'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-28 18:02:09 -06:00
										 |  |  |     it('should report missing event names in on- syntax', () => { | 
					
						
							|  |  |  |       expect(() => parse('<div on-></div>')).toThrowError(/Event name is missing in binding/); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |     it('should parse bound events and properties via [(...)]', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div [(prop)]="v"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['BoundAttribute', BindingType.Property, 'prop', 'v'], | 
					
						
							|  |  |  |         ['BoundEvent', 'propChange', null, 'v = $event'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse bound events and properties via bindon-', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div bindon-prop="v"></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['BoundAttribute', BindingType.Property, 'prop', 'v'], | 
					
						
							|  |  |  |         ['BoundEvent', 'propChange', null, 'v = $event'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-28 18:02:09 -06:00
										 |  |  |     it('should report missing property names in bindon- syntax', () => { | 
					
						
							|  |  |  |       expect(() => parse('<div bindon-></div>')) | 
					
						
							|  |  |  |           .toThrowError(/Property name is missing in binding/); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-27 14:39:07 -07:00
										 |  |  |     it('should report an error on empty expression', () => { | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |       expect(() => parse('<div (event)="">')).toThrowError(/Empty expressions are not allowed/); | 
					
						
							|  |  |  |       expect(() => parse('<div (event)="   ">')).toThrowError(/Empty expressions are not allowed/); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2020-12-02 11:14:07 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     it('should parse bound animation events when event name is empty', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div (@)="onAnimationEvent($event)"></div>', true).toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['BoundEvent', '', null, 'onAnimationEvent($event)'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |       expect(() => parse('<div (@)></div>')) | 
					
						
							|  |  |  |           .toThrowError(/Animation event name is missing in binding/); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should report invalid phase value of animation event', () => { | 
					
						
							|  |  |  |       expect(() => parse('<div (@event.invalidPhase)></div>')) | 
					
						
							|  |  |  |           .toThrowError( | 
					
						
							|  |  |  |               /The provided animation output phase value "invalidphase" for "@event" is not supported \(use start or done\)/); | 
					
						
							|  |  |  |       expect(() => parse('<div (@event.)></div>')) | 
					
						
							|  |  |  |           .toThrowError( | 
					
						
							|  |  |  |               /The animation trigger output event \(@event\) is missing its phase value name \(start or done are currently supported\)/); | 
					
						
							|  |  |  |       expect(() => parse('<div (@event)></div>')) | 
					
						
							|  |  |  |           .toThrowError( | 
					
						
							|  |  |  |               /The animation trigger output event \(@event\) is missing its phase value name \(start or done are currently supported\)/); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-12-28 18:02:09 -06:00
										 |  |  |   describe('variables', () => { | 
					
						
							|  |  |  |     it('should report variables not on template elements', () => { | 
					
						
							|  |  |  |       expect(() => parse('<div let-a-name="b"></div>')) | 
					
						
							|  |  |  |           .toThrowError(/"let-" is only supported on ng-template elements./); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should report missing variable names', () => { | 
					
						
							|  |  |  |       expect(() => parse('<ng-template let-><ng-template>')) | 
					
						
							|  |  |  |           .toThrowError(/Variable does not have a name/); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |   describe('references', () => { | 
					
						
							|  |  |  |     it('should parse references via #...', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div #a></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['Reference', 'a', ''], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse references via ref-', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div ref-a></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['Reference', 'a', ''], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse camel case references', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div #someA></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['Reference', 'someA', ''], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2019-12-28 18:02:09 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |     it('should report invalid reference names', () => { | 
					
						
							|  |  |  |       expect(() => parse('<div #a-b></div>')).toThrowError(/"-" is not allowed in reference names/); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should report missing reference names', () => { | 
					
						
							|  |  |  |       expect(() => parse('<div #></div>')).toThrowError(/Reference does not have a name/); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2021-01-23 22:10:12 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     it('should report an error if a reference is used multiple times on the same element', () => { | 
					
						
							|  |  |  |       expect(() => parse('<div #a #a></div>')) | 
					
						
							|  |  |  |           .toThrowError(/Reference "#a" is defined more than once/); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2019-12-28 18:02:09 -06:00
										 |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('literal attribute', () => { | 
					
						
							|  |  |  |     it('should report missing animation trigger in @ syntax', () => { | 
					
						
							|  |  |  |       expect(() => parse('<div @></div>')).toThrowError(/Animation trigger is missing/); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-27 14:39:07 -07:00
										 |  |  |   describe('ng-content', () => { | 
					
						
							|  |  |  |     it('should parse ngContent without selector', () => { | 
					
						
							|  |  |  |       const res = parse('<ng-content></ng-content>'); | 
					
						
							|  |  |  |       expectFromR3Nodes(res.nodes).toEqual([ | 
					
						
							| 
									
										
										
										
											2018-11-30 15:01:37 -08:00
										 |  |  |         ['Content', '*'], | 
					
						
							| 
									
										
										
										
											2018-04-27 14:39:07 -07:00
										 |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse ngContent with a specific selector', () => { | 
					
						
							|  |  |  |       const res = parse('<ng-content select="tag[attribute]"></ng-content>'); | 
					
						
							|  |  |  |       const selectors = ['', 'tag[attribute]']; | 
					
						
							|  |  |  |       expectFromR3Nodes(res.nodes).toEqual([ | 
					
						
							| 
									
										
										
										
											2018-11-30 15:01:37 -08:00
										 |  |  |         ['Content', selectors[1]], | 
					
						
							| 
									
										
										
										
											2018-04-27 14:39:07 -07:00
										 |  |  |         ['TextAttribute', 'select', selectors[1]], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse ngContent with a selector', () => { | 
					
						
							|  |  |  |       const res = parse( | 
					
						
							|  |  |  |           '<ng-content select="a"></ng-content><ng-content></ng-content><ng-content select="b"></ng-content>'); | 
					
						
							| 
									
										
										
										
											2018-11-30 15:01:37 -08:00
										 |  |  |       const selectors = ['*', 'a', 'b']; | 
					
						
							| 
									
										
										
										
											2018-04-27 14:39:07 -07:00
										 |  |  |       expectFromR3Nodes(res.nodes).toEqual([ | 
					
						
							| 
									
										
										
										
											2018-11-30 15:01:37 -08:00
										 |  |  |         ['Content', selectors[1]], | 
					
						
							| 
									
										
										
										
											2018-04-27 14:39:07 -07:00
										 |  |  |         ['TextAttribute', 'select', selectors[1]], | 
					
						
							| 
									
										
										
										
											2018-11-30 15:01:37 -08:00
										 |  |  |         ['Content', selectors[0]], | 
					
						
							|  |  |  |         ['Content', selectors[2]], | 
					
						
							| 
									
										
										
										
											2018-04-27 14:39:07 -07:00
										 |  |  |         ['TextAttribute', 'select', selectors[2]], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse ngProjectAs as an attribute', () => { | 
					
						
							|  |  |  |       const res = parse('<ng-content ngProjectAs="a"></ng-content>'); | 
					
						
							|  |  |  |       expectFromR3Nodes(res.nodes).toEqual([ | 
					
						
							| 
									
										
										
										
											2018-11-30 15:01:37 -08:00
										 |  |  |         ['Content', '*'], | 
					
						
							| 
									
										
										
										
											2018-04-27 14:39:07 -07:00
										 |  |  |         ['TextAttribute', 'ngProjectAs', 'a'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2018-04-24 14:22:55 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   describe('Ignored elements', () => { | 
					
						
							|  |  |  |     it('should ignore <script> elements', () => { | 
					
						
							|  |  |  |       expectFromHtml('<script></script>a').toEqual([ | 
					
						
							|  |  |  |         ['Text', 'a'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should ignore <style> elements', () => { | 
					
						
							|  |  |  |       expectFromHtml('<style></style>a').toEqual([ | 
					
						
							|  |  |  |         ['Text', 'a'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('<link rel="stylesheet">', () => { | 
					
						
							|  |  |  |     it('should keep <link rel="stylesheet"> elements if they have an absolute url', () => { | 
					
						
							|  |  |  |       expectFromHtml('<link rel="stylesheet" href="http://someurl">').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'link'], | 
					
						
							|  |  |  |         ['TextAttribute', 'rel', 'stylesheet'], | 
					
						
							|  |  |  |         ['TextAttribute', 'href', 'http://someurl'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |       expectFromHtml('<link REL="stylesheet" href="http://someurl">').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'link'], | 
					
						
							|  |  |  |         ['TextAttribute', 'REL', 'stylesheet'], | 
					
						
							|  |  |  |         ['TextAttribute', 'href', 'http://someurl'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should keep <link rel="stylesheet"> elements if they have no uri', () => { | 
					
						
							|  |  |  |       expectFromHtml('<link rel="stylesheet">').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'link'], | 
					
						
							|  |  |  |         ['TextAttribute', 'rel', 'stylesheet'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |       expectFromHtml('<link REL="stylesheet">').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'link'], | 
					
						
							|  |  |  |         ['TextAttribute', 'REL', 'stylesheet'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should ignore <link rel="stylesheet"> elements if they have a relative uri', () => { | 
					
						
							|  |  |  |       expectFromHtml('<link rel="stylesheet" href="./other.css">').toEqual([]); | 
					
						
							|  |  |  |       expectFromHtml('<link REL="stylesheet" HREF="./other.css">').toEqual([]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('ngNonBindable', () => { | 
					
						
							|  |  |  |     it('should ignore bindings on children of elements with ngNonBindable', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div ngNonBindable>{{b}}</div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['TextAttribute', 'ngNonBindable', ''], | 
					
						
							|  |  |  |         ['Text', '{{b}}'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should keep nested children of elements with ngNonBindable', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div ngNonBindable><span>{{b}}</span></div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['TextAttribute', 'ngNonBindable', ''], | 
					
						
							|  |  |  |         ['Element', 'span'], | 
					
						
							|  |  |  |         ['Text', '{{b}}'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should ignore <script> elements inside of elements with ngNonBindable', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div ngNonBindable><script></script>a</div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['TextAttribute', 'ngNonBindable', ''], | 
					
						
							|  |  |  |         ['Text', 'a'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should ignore <style> elements inside of elements with ngNonBindable', () => { | 
					
						
							|  |  |  |       expectFromHtml('<div ngNonBindable><style></style>a</div>').toEqual([ | 
					
						
							|  |  |  |         ['Element', 'div'], | 
					
						
							|  |  |  |         ['TextAttribute', 'ngNonBindable', ''], | 
					
						
							|  |  |  |         ['Text', 'a'], | 
					
						
							|  |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should ignore <link rel="stylesheet"> elements inside of elements with ngNonBindable', | 
					
						
							|  |  |  |        () => { | 
					
						
							|  |  |  |          expectFromHtml('<div ngNonBindable><link rel="stylesheet">a</div>').toEqual([ | 
					
						
							|  |  |  |            ['Element', 'div'], | 
					
						
							|  |  |  |            ['TextAttribute', 'ngNonBindable', ''], | 
					
						
							|  |  |  |            ['Text', 'a'], | 
					
						
							|  |  |  |          ]); | 
					
						
							|  |  |  |        }); | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2019-02-15 21:55:07 +01:00
										 |  |  | }); |