/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import { Reflector } from '../src/reflection/reflection'; import { makeDecorator, makeParamDecorator, makePropDecorator, makePropTypeDecorator } from '../src/util/decorators'; import { ReflectionCapabilities, isDelegateCtor } from '../src/reflection/reflection_capabilities'; interface ClassDecoratorFactory { (data: ClassDecorator): any; new (data: ClassDecorator): ClassDecorator; } interface ClassDecorator { value: any; } interface ParamDecorator { value: any; } interface PropDecorator { value: any; } /** @Annotation */ const ClassDecorator = makeDecorator('ClassDecorator', (data: any) => data) as ClassDecoratorFactory; /** @Annotation */ const ParamDecorator = makeParamDecorator('ParamDecorator', (value: any) => ({ value })); /** @Annotation */ const PropDecorator = makePropDecorator('PropDecorator', (value: any) => ({ value })); /** @Annotation */ const PropTypeDecorator = makePropTypeDecorator('PropTypeDecorator', (value: any) => ({ value })); class AType { constructor(public value: any) {} } @ClassDecorator({ value: 'class' }) class ClassWithDecorators { @PropDecorator('p1') @PropDecorator('p2') a: AType; b: AType; @PropDecorator('p3') set c(value: any) {} @PropTypeDecorator('type') d: number; @PropDecorator('p4') someMethod() {} constructor(@ParamDecorator('a') a: AType, @ParamDecorator('b') b: AType, d: AType) { this.a = a; this.b = b; } } class ClassWithoutDecorators { constructor(a: any, b: any) {} } class TestObj { constructor(public a: any, public b: any) {} identity(arg: any) { return arg; } } { describe('Reflector', () => { let reflector: Reflector; beforeEach(() => { reflector = new Reflector(new ReflectionCapabilities()); }); describe('factory', () => { it('should create a factory for the given type', () => { const obj = reflector.factory(TestObj)(1, 2); expect(obj.a).toEqual(1); expect(obj.b).toEqual(2); }); }); describe('parameters', () => { it('should return an array of parameters for a type', () => { const p = reflector.parameters(ClassWithDecorators); expect(p).toEqual([[AType, new ParamDecorator('a')], [AType, new ParamDecorator('b')], [AType]]); }); it('should work for a class without annotations', () => { const p = reflector.parameters(ClassWithoutDecorators); expect(p.length).toEqual(2); }); // See https://github.com/angular/tsickle/issues/261 it('should read forwardRef down-leveled type', () => { class Dep {} class ForwardLegacy { constructor(d: Dep) {} // Older tsickle had a bug: wrote a forward reference static ctorParameters = [{ type: Dep }]; } expect(reflector.parameters(ForwardLegacy)).toEqual([[Dep]]); class Forward { constructor(d: Dep) {} // Newer tsickle generates a functionClosure static ctorParameters = () => [{ type: ForwardDep }]; } class ForwardDep {} expect(reflector.parameters(Forward)).toEqual([[ForwardDep]]); }); it('should not return undefined types for downleveled types', () => { class Dep {} class TestService { constructor() {} static ctorParameters = () => [{ type: undefined, decorators: [] }, { type: Dep }]; } expect(reflector.parameters(TestService)).toEqual([[], [Dep]]); }); }); describe('propMetadata', () => { it('should return a string map of prop metadata for the given class', () => { const p = reflector.propMetadata(ClassWithDecorators); expect(p.a).toEqual([new PropDecorator('p1'), new PropDecorator('p2')]); expect(p.c).toEqual([new PropDecorator('p3')]); expect(p.d).toEqual([Number, new PropTypeDecorator('type')]); expect(p.someMethod).toEqual([new PropDecorator('p4')]); }); it('should also return metadata if the class has no decorator', () => { class Test { @PropDecorator('test') prop1: any; @PropTypeDecorator('test type') prop2: string; } expect(reflector.propMetadata(Test)).toEqual({ prop1: [new PropDecorator('test')], prop2: [String, new PropTypeDecorator('test type')], }); }); }); describe('annotations', () => { it('should return an array of annotations for a type', () => { const p = reflector.annotations(ClassWithDecorators); expect(p).toEqual([new ClassDecorator({ value: 'class' })]); }); it('should work for a class without annotations', () => { const p = reflector.annotations(ClassWithoutDecorators); expect(p).toEqual([]); }); }); describe('getter', () => { it('returns a function reading a property', () => { const getA = reflector.getter('a'); expect(getA(new TestObj(1, 2))).toEqual(1); }); }); describe('setter', () => { it('returns a function setting a property', () => { const setA = reflector.setter('a'); const obj = new TestObj(1, 2); setA(obj, 100); expect(obj.a).toEqual(100); }); }); describe('method', () => { it('returns a function invoking a method', () => { const func = reflector.method('identity'); const obj = new TestObj(1, 2); expect(func(obj, ['value'])).toEqual('value'); }); }); describe('isDelegateCtor', () => { it('should support ES5 compiled classes', () => { // These classes will be compiled to ES5 code so their stringified form // below will contain ES5 constructor functions rather than native classes. class Parent {} class ChildNoCtor extends Parent {} class ChildWithCtor extends Parent { constructor() { super(); } } class ChildNoCtorPrivateProps extends Parent { private x = 10; } expect(isDelegateCtor(ChildNoCtor.toString())).toBe(true); expect(isDelegateCtor(ChildNoCtorPrivateProps.toString())).toBe(true); expect(isDelegateCtor(ChildWithCtor.toString())).toBe(false); }); it('should not throw when no prototype on type', () => { // Cannot test arrow function here due to the compilation const dummyArrowFn = function() {}; Object.defineProperty(dummyArrowFn, 'prototype', { value: undefined }); expect(() => reflector.annotations(dummyArrowFn as any)).not.toThrow(); }); it('should support native class', () => { // These classes are defined as strings unlike the tests above because otherwise // the compiler (of these tests) will convert them to ES5 constructor function // style classes. const ChildNoCtor = `class ChildNoCtor extends Parent {}\n`; const ChildWithCtor = `class ChildWithCtor extends Parent {\n` + ` constructor() { super(); }` + `}\n`; const ChildNoCtorComplexBase = `class ChildNoCtor extends Parent['foo'].bar(baz) {}\n`; const ChildWithCtorComplexBase = `class ChildWithCtor extends Parent['foo'].bar(baz) {\n` + ` constructor() { super(); }` + `}\n`; const ChildNoCtorPrivateProps = `class ChildNoCtorPrivateProps extends Parent {\n` + ` constructor() {\n` + // Note that the instance property causes a pass-through constructor to be synthesized ` super(...arguments);\n` + ` this.x = 10;\n` + ` }\n` + `}\n`; expect(isDelegateCtor(ChildNoCtor)).toBe(true); expect(isDelegateCtor(ChildNoCtorPrivateProps)).toBe(true); expect(isDelegateCtor(ChildWithCtor)).toBe(false); expect(isDelegateCtor(ChildNoCtorComplexBase)).toBe(true); expect(isDelegateCtor(ChildWithCtorComplexBase)).toBe(false); }); it('should properly handle all class forms', () => { const ctor = (str: string) => expect(isDelegateCtor(str)).toBe(false); const noCtor = (str: string) => expect(isDelegateCtor(str)).toBe(true); ctor(`class Bar extends Foo {constructor(){}}`); ctor(`class Bar extends Foo { constructor ( ) {} }`); ctor(`class Bar extends Foo { other(){}; constructor(){} }`); noCtor(`class extends Foo{}`); noCtor(`class extends Foo {}`); noCtor(`class Bar extends Foo {}`); noCtor(`class $Bar1_ extends $Fo0_ {}`); noCtor(`class Bar extends Foo { other(){} }`); }); }); describe('inheritance with decorators', () => { it('should inherit annotations', () => { @ClassDecorator({ value: 'parent' }) class Parent {} @ClassDecorator({ value: 'child' }) class Child extends Parent {} class ChildNoDecorators extends Parent {} class NoDecorators {} // Check that metadata for Parent was not changed! expect(reflector.annotations(Parent)).toEqual([new ClassDecorator({ value: 'parent' })]); expect(reflector.annotations(Child)).toEqual([ new ClassDecorator({ value: 'parent' }), new ClassDecorator({ value: 'child' }), ]); expect(reflector.annotations(ChildNoDecorators)).toEqual([new ClassDecorator({ value: 'parent' })]); expect(reflector.annotations(NoDecorators)).toEqual([]); expect(reflector.annotations({} as any)).toEqual([]); expect(reflector.annotations(1 as any)).toEqual([]); expect(reflector.annotations(null!)).toEqual([]); }); it('should inherit parameters', () => { class A {} class B {} class C {} // Note: We need the class decorator as well, // as otherwise TS won't capture the ctor arguments! @ClassDecorator({ value: 'parent' }) class Parent { constructor(@ParamDecorator('a') a: A, @ParamDecorator('b') b: B) {} } class Child extends Parent {} @ClassDecorator({ value: 'child' }) class ChildWithDecorator extends Parent {} @ClassDecorator({ value: 'child' }) class ChildWithDecoratorAndProps extends Parent { private x = 10; } // Note: We need the class decorator as well, // as otherwise TS won't capture the ctor arguments! @ClassDecorator({ value: 'child' }) class ChildWithCtor extends Parent { constructor(@ParamDecorator('c') c: C) { super(null!, null!); } } class ChildWithCtorNoDecorator extends Parent { constructor(a: any, b: any, c: any) { super(null!, null!); } } class NoDecorators {} // Check that metadata for Parent was not changed! expect(reflector.parameters(Parent)).toEqual([ [A, new ParamDecorator('a')], [B, new ParamDecorator('b')], ]); expect(reflector.parameters(Child)).toEqual([ [A, new ParamDecorator('a')], [B, new ParamDecorator('b')], ]); expect(reflector.parameters(ChildWithDecorator)).toEqual([ [A, new ParamDecorator('a')], [B, new ParamDecorator('b')], ]); expect(reflector.parameters(ChildWithDecoratorAndProps)).toEqual([ [A, new ParamDecorator('a')], [B, new ParamDecorator('b')], ]); expect(reflector.parameters(ChildWithCtor)).toEqual([[C, new ParamDecorator('c')]]); // If we have no decorator, we don't get metadata about the ctor params. // But we should still get an array of the right length based on function.length. // TODO: Review use of `any` here (#19904) expect(reflector.parameters(ChildWithCtorNoDecorator)).toEqual([undefined, undefined, undefined] as any[]); expect(reflector.parameters(NoDecorators)).toEqual([]); expect(reflector.parameters({} as any)).toEqual([]); expect(reflector.parameters(1 as any)).toEqual([]); expect(reflector.parameters(null!)).toEqual([]); }); it('should inherit property metadata', () => { class A {} class B {} class C {} class D {} class Parent { @PropDecorator('a') a: A; @PropDecorator('b1') b: B; @PropTypeDecorator('type parent') d: D; } class Child extends Parent { @PropDecorator('b2') b: B; @PropDecorator('c') c: C; @PropTypeDecorator('type child') d: D; } class NoDecorators {} // Check that metadata for Parent was not changed! expect(reflector.propMetadata(Parent)).toEqual({ a: [new PropDecorator('a')], b: [new PropDecorator('b1')], d: [D, new PropTypeDecorator('type parent')], }); expect(reflector.propMetadata(Child)).toEqual({ a: [new PropDecorator('a')], b: [new PropDecorator('b1'), new PropDecorator('b2')], d: [D, new PropTypeDecorator('type parent'), D, new PropTypeDecorator('type child')], c: [new PropDecorator('c')], }); expect(reflector.propMetadata(NoDecorators)).toEqual({}); expect(reflector.propMetadata({} as any)).toEqual({}); expect(reflector.propMetadata(1 as any)).toEqual({}); expect(reflector.propMetadata(null!)).toEqual({}); }); it('should inherit lifecycle hooks', () => { class Parent { hook1() {} hook2() {} } class Child extends Parent { hook2() {} hook3() {} } function hooks(symbol: any, names: string[]): boolean[] { return names.map(name => reflector.hasLifecycleHook(symbol, name)); } // Check that metadata for Parent was not changed! expect(hooks(Parent, ['hook1', 'hook2', 'hook3'])).toEqual([true, true, false]); expect(hooks(Child, ['hook1', 'hook2', 'hook3'])).toEqual([true, true, true]); }); }); describe('inheritance with tsickle', () => { it('should inherit annotations', () => { class Parent { static decorators = [{ type: ClassDecorator, args: [{ value: 'parent' }] }]; } class Child extends Parent { static decorators = [{ type: ClassDecorator, args: [{ value: 'child' }] }]; } class ChildNoDecorators extends Parent {} // Check that metadata for Parent was not changed! expect(reflector.annotations(Parent)).toEqual([new ClassDecorator({ value: 'parent' })]); expect(reflector.annotations(Child)).toEqual([ new ClassDecorator({ value: 'parent' }), new ClassDecorator({ value: 'child' }), ]); expect(reflector.annotations(ChildNoDecorators)).toEqual([new ClassDecorator({ value: 'parent' })]); }); it('should inherit parameters', () => { class A {} class B {} class C {} class Parent { static ctorParameters = () => [ { type: A, decorators: [{ type: ParamDecorator, args: ['a'] }] }, { type: B, decorators: [{ type: ParamDecorator, args: ['b'] }] }, ]; } class Child extends Parent {} class ChildWithCtor extends Parent { static ctorParameters = () => [{ type: C, decorators: [{ type: ParamDecorator, args: ['c'] }] }]; constructor() { super(); } } // Check that metadata for Parent was not changed! expect(reflector.parameters(Parent)).toEqual([ [A, new ParamDecorator('a')], [B, new ParamDecorator('b')], ]); expect(reflector.parameters(Child)).toEqual([ [A, new ParamDecorator('a')], [B, new ParamDecorator('b')], ]); expect(reflector.parameters(ChildWithCtor)).toEqual([[C, new ParamDecorator('c')]]); }); it('should inherit property metadata', () => { class A {} class B {} class C {} class Parent { static propDecorators: any = { a: [{ type: PropDecorator, args: ['a'] }], b: [{ type: PropDecorator, args: ['b1'] }], }; } class Child extends Parent { static propDecorators: any = { b: [{ type: PropDecorator, args: ['b2'] }], c: [{ type: PropDecorator, args: ['c'] }], }; } // Check that metadata for Parent was not changed! expect(reflector.propMetadata(Parent)).toEqual({ a: [new PropDecorator('a')], b: [new PropDecorator('b1')], }); expect(reflector.propMetadata(Child)).toEqual({ a: [new PropDecorator('a')], b: [new PropDecorator('b1'), new PropDecorator('b2')], c: [new PropDecorator('c')], }); }); }); describe('inheritance with es5 API', () => { it('should inherit annotations', () => { class Parent { static annotations = [new ClassDecorator({ value: 'parent' })]; } class Child extends Parent { static annotations = [new ClassDecorator({ value: 'child' })]; } class ChildNoDecorators extends Parent {} // Check that metadata for Parent was not changed! expect(reflector.annotations(Parent)).toEqual([new ClassDecorator({ value: 'parent' })]); expect(reflector.annotations(Child)).toEqual([ new ClassDecorator({ value: 'parent' }), new ClassDecorator({ value: 'child' }), ]); expect(reflector.annotations(ChildNoDecorators)).toEqual([new ClassDecorator({ value: 'parent' })]); }); it('should inherit parameters', () => { class A {} class B {} class C {} class Parent { static parameters = [ [A, new ParamDecorator('a')], [B, new ParamDecorator('b')], ]; } class Child extends Parent {} class ChildWithCtor extends Parent { static parameters = [[C, new ParamDecorator('c')]]; constructor() { super(); } } // Check that metadata for Parent was not changed! expect(reflector.parameters(Parent)).toEqual([ [A, new ParamDecorator('a')], [B, new ParamDecorator('b')], ]); expect(reflector.parameters(Child)).toEqual([ [A, new ParamDecorator('a')], [B, new ParamDecorator('b')], ]); expect(reflector.parameters(ChildWithCtor)).toEqual([[C, new ParamDecorator('c')]]); }); it('should inherit property metadata', () => { class A {} class B {} class C {} class Parent { static propMetadata: any = { a: [new PropDecorator('a')], b: [new PropDecorator('b1')], }; } class Child extends Parent { static propMetadata: any = { b: [new PropDecorator('b2')], c: [new PropDecorator('c')], }; } // Check that metadata for Parent was not changed! expect(reflector.propMetadata(Parent)).toEqual({ a: [new PropDecorator('a')], b: [new PropDecorator('b1')], }); expect(reflector.propMetadata(Child)).toEqual({ a: [new PropDecorator('a')], b: [new PropDecorator('b1'), new PropDecorator('b2')], c: [new PropDecorator('c')], }); }); }); }); }