/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {core} from '@angular/compiler'; import {DirectiveResolver} from '@angular/compiler/src/directive_resolver'; import {Component, ContentChild, ContentChildren, Directive, HostBinding, HostListener, Input, Output, ViewChild, ViewChildren} from '@angular/core/src/metadata'; import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_reflector'; @Directive({selector: 'someDirective'}) class SomeDirective { } @Directive({selector: 'someDirective', inputs: ['c']}) class SomeDirectiveWithInputs { @Input() a: any; @Input('renamed') b: any; c: any; } @Directive({selector: 'someDirective', outputs: ['c']}) class SomeDirectiveWithOutputs { @Output() a: any; @Output('renamed') b: any; c: any; } @Directive({selector: 'someDirective'}) class SomeDirectiveWithSetterProps { @Input('renamed') set a(value: any) { } } @Directive({selector: 'someDirective'}) class SomeDirectiveWithGetterOutputs { @Output('renamed') get a(): any { return null; } } @Directive({selector: 'someDirective', host: {'[c]': 'c'}}) class SomeDirectiveWithHostBindings { @HostBinding() a: any; @HostBinding('renamed') b: any; c: any; } @Directive({selector: 'someDirective', host: {'(c)': 'onC()'}}) class SomeDirectiveWithHostListeners { @HostListener('a') onA() { } @HostListener('b', ['$event.value']) onB(value: any) { } } @Directive({selector: 'someDirective', queries: {'cs': new ContentChildren('c')}}) class SomeDirectiveWithContentChildren { @ContentChildren('a') as: any; c: any; } @Directive({selector: 'someDirective', queries: {'cs': new ViewChildren('c')}}) class SomeDirectiveWithViewChildren { @ViewChildren('a') as: any; c: any; } @Directive({selector: 'someDirective', queries: {'c': new ContentChild('c')}}) class SomeDirectiveWithContentChild { @ContentChild('a') a: any; c: any; } @Directive({selector: 'someDirective', queries: {'c': new ViewChild('c')}}) class SomeDirectiveWithViewChild { @ViewChild('a') a: any; c: any; } @Component({ selector: 'sample', template: 'some template', styles: ['some styles'], preserveWhitespaces: true }) class ComponentWithTemplate { } @Directive({ selector: 'someDirective', host: {'[decorator]': 'decorator'}, inputs: ['decorator'], }) class SomeDirectiveWithSameHostBindingAndInput { @Input() @HostBinding() prop: any; } @Directive({selector: 'someDirective'}) class SomeDirectiveWithMalformedHostBinding1 { @HostBinding('(a)') onA() { } } @Directive({selector: 'someDirective'}) class SomeDirectiveWithMalformedHostBinding2 { @HostBinding('[a]') onA() { } } class SomeDirectiveWithoutMetadata {} { describe('DirectiveResolver', () => { let resolver: DirectiveResolver; beforeEach(() => { resolver = new DirectiveResolver(new JitReflector()); }); it('should read out the Directive metadata', () => { const directiveMetadata = resolver.resolve(SomeDirective); expect(directiveMetadata).toEqual(core.createDirective({ selector: 'someDirective', inputs: [], outputs: [], host: {}, queries: {}, guards: {}, exportAs: undefined, providers: undefined })); }); it('should throw if not matching metadata is found', () => { expect(() => { resolver.resolve(SomeDirectiveWithoutMetadata); }).toThrowError('No Directive annotation found on SomeDirectiveWithoutMetadata'); }); it('should support inheriting the Directive metadata', function() { @Directive({selector: 'p'}) class Parent { } class ChildNoDecorator extends Parent {} @Directive({selector: 'c'}) class ChildWithDecorator extends Parent { } expect(resolver.resolve(ChildNoDecorator)).toEqual(core.createDirective({ selector: 'p', inputs: [], outputs: [], host: {}, queries: {}, guards: {}, exportAs: undefined, providers: undefined })); expect(resolver.resolve(ChildWithDecorator)).toEqual(core.createDirective({ selector: 'c', inputs: [], outputs: [], host: {}, queries: {}, guards: {}, exportAs: undefined, providers: undefined })); }); describe('inputs', () => { it('should append directive inputs', () => { const directiveMetadata = resolver.resolve(SomeDirectiveWithInputs); expect(directiveMetadata.inputs).toEqual(['c', 'a', 'b: renamed']); }); it('should work with getters and setters', () => { const directiveMetadata = resolver.resolve(SomeDirectiveWithSetterProps); expect(directiveMetadata.inputs).toEqual(['a: renamed']); }); it('should remove duplicate inputs', () => { @Directive({selector: 'someDirective', inputs: ['a', 'a']}) class SomeDirectiveWithDuplicateInputs { } const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateInputs); expect(directiveMetadata.inputs).toEqual(['a']); }); it('should use the last input if duplicate inputs (with rename)', () => { @Directive({selector: 'someDirective', inputs: ['a', 'localA: a']}) class SomeDirectiveWithDuplicateInputs { } const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateInputs); expect(directiveMetadata.inputs).toEqual(['localA: a']); }); it('should prefer @Input over @Directive.inputs', () => { @Directive({selector: 'someDirective', inputs: ['a']}) class SomeDirectiveWithDuplicateInputs { @Input('a') propA: any; } const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateInputs); expect(directiveMetadata.inputs).toEqual(['propA: a']); }); it('should support inheriting inputs', () => { @Directive({selector: 'p'}) class Parent { @Input() p1: any; @Input('p21') p2: any; } class Child extends Parent { @Input('p22') override p2: any; @Input() p3: any; } const directiveMetadata = resolver.resolve(Child); expect(directiveMetadata.inputs).toEqual(['p1', 'p2: p22', 'p3']); }); }); describe('outputs', () => { it('should append directive outputs', () => { const directiveMetadata = resolver.resolve(SomeDirectiveWithOutputs); expect(directiveMetadata.outputs).toEqual(['c', 'a', 'b: renamed']); }); it('should work with getters and setters', () => { const directiveMetadata = resolver.resolve(SomeDirectiveWithGetterOutputs); expect(directiveMetadata.outputs).toEqual(['a: renamed']); }); it('should remove duplicate outputs', () => { @Directive({selector: 'someDirective', outputs: ['a', 'a']}) class SomeDirectiveWithDuplicateOutputs { } const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateOutputs); expect(directiveMetadata.outputs).toEqual(['a']); }); it('should use the last output if duplicate outputs (with rename)', () => { @Directive({selector: 'someDirective', outputs: ['a', 'localA: a']}) class SomeDirectiveWithDuplicateOutputs { } const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateOutputs); expect(directiveMetadata.outputs).toEqual(['localA: a']); }); it('should prefer @Output over @Directive.outputs', () => { @Directive({selector: 'someDirective', outputs: ['a']}) class SomeDirectiveWithDuplicateOutputs { @Output('a') propA: any; } const directiveMetadata = resolver.resolve(SomeDirectiveWithDuplicateOutputs); expect(directiveMetadata.outputs).toEqual(['propA: a']); }); it('should support inheriting outputs', () => { @Directive({selector: 'p'}) class Parent { @Output() p1: any; @Output('p21') p2: any; } class Child extends Parent { @Output('p22') override p2: any; @Output() p3: any; } const directiveMetadata = resolver.resolve(Child); expect(directiveMetadata.outputs).toEqual(['p1', 'p2: p22', 'p3']); }); }); describe('host', () => { it('should append host bindings', () => { const directiveMetadata = resolver.resolve(SomeDirectiveWithHostBindings); expect(directiveMetadata.host).toEqual({'[c]': 'c', '[a]': 'a', '[renamed]': 'b'}); }); it('should append host binding and input on the same property', () => { const directiveMetadata = resolver.resolve(SomeDirectiveWithSameHostBindingAndInput); expect(directiveMetadata.host).toEqual({'[decorator]': 'decorator', '[prop]': 'prop'}); expect(directiveMetadata.inputs).toEqual(['decorator', 'prop']); }); it('should append host listeners', () => { const directiveMetadata = resolver.resolve(SomeDirectiveWithHostListeners); expect(directiveMetadata.host) .toEqual({'(c)': 'onC()', '(a)': 'onA()', '(b)': 'onB($event.value)'}); }); it('should throw when @HostBinding name starts with "("', () => { expect(() => resolver.resolve(SomeDirectiveWithMalformedHostBinding1)) .toThrowError('@HostBinding can not bind to events. Use @HostListener instead.'); }); it('should throw when @HostBinding name starts with "["', () => { expect(() => resolver.resolve(SomeDirectiveWithMalformedHostBinding2)) .toThrowError( `@HostBinding parameter should be a property name, 'class.', or 'attr.'.`); }); it('should support inheriting host bindings', () => { @Directive({selector: 'p'}) class Parent { @HostBinding() p1: any; @HostBinding('p21') p2: any; } class Child extends Parent { @HostBinding('p22') override p2: any; @HostBinding() p3: any; } const directiveMetadata = resolver.resolve(Child); expect(directiveMetadata.host) .toEqual({'[p1]': 'p1', '[p21]': 'p2', '[p22]': 'p2', '[p3]': 'p3'}); }); it('should support inheriting host listeners', () => { @Directive({selector: 'p'}) class Parent { @HostListener('p1') p1() { } @HostListener('p21') p2() { } } class Child extends Parent { @HostListener('p22') override p2() { } @HostListener('p3') p3() { } } const directiveMetadata = resolver.resolve(Child); expect(directiveMetadata.host) .toEqual({'(p1)': 'p1()', '(p21)': 'p2()', '(p22)': 'p2()', '(p3)': 'p3()'}); }); it('should combine host bindings and listeners during inheritance', () => { @Directive({selector: 'p'}) class Parent { @HostListener('p11') @HostListener('p12') p1() { } @HostBinding('p21') @HostBinding('p22') p2: any; } class Child extends Parent { @HostListener('c1') override p1() { } @HostBinding('c2') override p2: any; } const directiveMetadata = resolver.resolve(Child); expect(directiveMetadata.host).toEqual({ '(p11)': 'p1()', '(p12)': 'p1()', '(c1)': 'p1()', '[p21]': 'p2', '[p22]': 'p2', '[c2]': 'p2' }); }); }); describe('queries', () => { it('should append ContentChildren', () => { const directiveMetadata = resolver.resolve(SomeDirectiveWithContentChildren); expect(directiveMetadata.queries) .toEqual({'cs': new ContentChildren('c'), 'as': new ContentChildren('a')}); }); it('should append ViewChildren', () => { const directiveMetadata = resolver.resolve(SomeDirectiveWithViewChildren); expect(directiveMetadata.queries) .toEqual({'cs': new ViewChildren('c'), 'as': new ViewChildren('a')}); }); it('should append ContentChild', () => { const directiveMetadata = resolver.resolve(SomeDirectiveWithContentChild); expect(directiveMetadata.queries) .toEqual({'c': new ContentChild('c'), 'a': new ContentChild('a')}); }); it('should append ViewChild', () => { const directiveMetadata = resolver.resolve(SomeDirectiveWithViewChild); expect(directiveMetadata.queries) .toEqual({'c': new ViewChild('c'), 'a': new ViewChild('a')}); }); it('should support inheriting queries', () => { @Directive({selector: 'p'}) class Parent { @ContentChild('p1') p1: any; @ContentChild('p21') p2: any; } class Child extends Parent { @ContentChild('p22') override p2: any; @ContentChild('p3') p3: any; } const directiveMetadata = resolver.resolve(Child); expect(directiveMetadata.queries).toEqual({ 'p1': new ContentChild('p1'), 'p2': new ContentChild('p22'), 'p3': new ContentChild('p3') }); }); }); describe('Component', () => { it('should read out the template related metadata from the Component metadata', () => { const compMetadata: Component = resolver.resolve(ComponentWithTemplate); expect(compMetadata.template).toEqual('some template'); expect(compMetadata.styles).toEqual(['some styles']); expect(compMetadata.preserveWhitespaces).toBe(true); }); }); }); }