import * as ts from 'typescript'; import {MetadataCollector} from '../src/collector'; import {ClassMetadata, ConstructorMetadata, ModuleMetadata} from '../src/schema'; import {Directory, expectValidSources, Host} from './typescript.mocks'; describe('Collector', () => { let host: ts.LanguageServiceHost; let service: ts.LanguageService; let program: ts.Program; let collector: MetadataCollector; beforeEach(() => { host = new Host(FILES, [ '/app/app.component.ts', '/app/cases-data.ts', '/app/error-cases.ts', '/promise.ts', '/unsupported-1.ts', '/unsupported-2.ts' ]); service = ts.createLanguageService(host); program = service.getProgram(); collector = new MetadataCollector(); }); it('should not have errors in test data', () => { expectValidSources(service, program); }); it('should return undefined for modules that have no metadata', () => { const sourceFile = program.getSourceFile('app/hero.ts'); const metadata = collector.getMetadata(sourceFile); expect(metadata).toBeUndefined(); }); it('should be able to collect a simple component\'s metadata', () => { const sourceFile = program.getSourceFile('app/hero-detail.component.ts'); const metadata = collector.getMetadata(sourceFile); expect(metadata).toEqual({ __symbolic: 'module', version: 1, metadata: { HeroDetailComponent: { __symbolic: 'class', decorators: [{ __symbolic: 'call', expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'}, arguments: [{ selector: 'my-hero-detail', template: `

{{hero.name}} details!

{{hero.id}}
` }] }], members: { hero: [{ __symbolic: 'property', decorators: [{ __symbolic: 'call', expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Input'} }] }] } } } }); }); it('should be able to get a more complicated component\'s metadata', () => { const sourceFile = program.getSourceFile('/app/app.component.ts'); const metadata = collector.getMetadata(sourceFile); expect(metadata).toEqual({ __symbolic: 'module', version: 1, metadata: { AppComponent: { __symbolic: 'class', decorators: [{ __symbolic: 'call', expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'}, arguments: [{ selector: 'my-app', template: `

My Heroes

`, directives: [ { __symbolic: 'reference', module: './hero-detail.component', name: 'HeroDetailComponent', }, {__symbolic: 'reference', module: 'angular2/common', name: 'NgFor'} ], providers: [{__symbolic: 'reference', module: './hero.service', default: true}], pipes: [ {__symbolic: 'reference', module: 'angular2/common', name: 'LowerCasePipe'}, {__symbolic: 'reference', module: 'angular2/common', name: 'UpperCasePipe'} ] }] }], members: { __ctor__: [{ __symbolic: 'constructor', parameters: [{__symbolic: 'reference', module: './hero.service', default: true}] }], onSelect: [{__symbolic: 'method'}], ngOnInit: [{__symbolic: 'method'}], getHeroes: [{__symbolic: 'method'}] } } } }); }); it('should return the values of exported variables', () => { const sourceFile = program.getSourceFile('/app/mock-heroes.ts'); const metadata = collector.getMetadata(sourceFile); expect(metadata).toEqual({ __symbolic: 'module', version: 1, metadata: { HEROES: [ {'id': 11, 'name': 'Mr. Nice'}, {'id': 12, 'name': 'Narco'}, {'id': 13, 'name': 'Bombasto'}, {'id': 14, 'name': 'Celeritas'}, {'id': 15, 'name': 'Magneta'}, {'id': 16, 'name': 'RubberMan'}, {'id': 17, 'name': 'Dynama'}, {'id': 18, 'name': 'Dr IQ'}, {'id': 19, 'name': 'Magma'}, {'id': 20, 'name': 'Tornado'} ] } }); }); it('should return undefined for modules that have no metadata', () => { const sourceFile = program.getSourceFile('/app/error-cases.ts'); expect(sourceFile).toBeTruthy(sourceFile); const metadata = collector.getMetadata(sourceFile); expect(metadata).toBeUndefined(); }); let casesFile: ts.SourceFile; let casesMetadata: ModuleMetadata; beforeEach(() => { casesFile = program.getSourceFile('/app/cases-data.ts'); casesMetadata = collector.getMetadata(casesFile); }); it('should provide any reference for an any ctor parameter type', () => { const casesAny = casesMetadata.metadata['CaseAny']; expect(casesAny).toBeTruthy(); const ctorData = casesAny.members['__ctor__']; expect(ctorData).toEqual( [{__symbolic: 'constructor', parameters: [{__symbolic: 'reference', name: 'any'}]}]); }); it('should record annotations on set and get declarations', () => { const propertyData = { name: [{ __symbolic: 'property', decorators: [{ __symbolic: 'call', expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Input'}, arguments: ['firstName'] }] }] }; const caseGetProp = casesMetadata.metadata['GetProp']; expect(caseGetProp.members).toEqual(propertyData); const caseSetProp = casesMetadata.metadata['SetProp']; expect(caseSetProp.members).toEqual(propertyData); const caseFullProp = casesMetadata.metadata['FullProp']; expect(caseFullProp.members).toEqual(propertyData); }); it('should record references to parameterized types', () => { const casesForIn = casesMetadata.metadata['NgFor']; expect(casesForIn).toEqual({ __symbolic: 'class', decorators: [{ __symbolic: 'call', expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Injectable'} }], members: { __ctor__: [{ __symbolic: 'constructor', parameters: [{ __symbolic: 'reference', name: 'ClassReference', arguments: [{__symbolic: 'reference', name: 'NgForRow'}] }] }] } }); }); it('should report errors for destructured imports', () => { let unsupported1 = program.getSourceFile('/unsupported-1.ts'); let metadata = collector.getMetadata(unsupported1); expect(metadata).toEqual({ __symbolic: 'module', version: 1, metadata: { a: { __symbolic: 'error', message: 'Destructuring declarations cannot be referenced statically', line: 1, character: 16 }, b: { __symbolic: 'error', message: 'Destructuring declarations cannot be referenced statically', line: 1, character: 18 }, c: { __symbolic: 'error', message: 'Destructuring declarations cannot be referenced statically', line: 2, character: 16 }, d: { __symbolic: 'error', message: 'Destructuring declarations cannot be referenced statically', line: 2, character: 18 }, e: { __symbolic: 'error', message: 'Only intialized variables and constants can be referenced statically', line: 3, character: 14 } } }); }); it('should report an error for refrences to unexpected types', () => { let unsupported1 = program.getSourceFile('/unsupported-2.ts'); let metadata = collector.getMetadata(unsupported1); let barClass = metadata.metadata['Bar']; let ctor = barClass.members['__ctor__'][0]; let parameter = ctor.parameters[0]; expect(parameter).toEqual({ __symbolic: 'error', message: 'Reference to non-exported class Foo', line: 1, character: 45 }); }); }); // TODO: Do not use \` in a template literal as it confuses clang-format const FILES: Directory = { 'app': { 'app.component.ts': ` import {Component as MyComponent, OnInit} from 'angular2/core'; import * as common from 'angular2/common'; import {Hero} from './hero'; import {HeroDetailComponent} from './hero-detail.component'; import HeroService from './hero.service'; // thrown away import 'angular2/core'; @MyComponent({ selector: 'my-app', template:` + '`' + `

My Heroes

  • {{hero.id | lowercase}} {{hero.name | uppercase}}
` + '`' + `, directives: [HeroDetailComponent, common.NgFor], providers: [HeroService], pipes: [common.LowerCasePipe, common.UpperCasePipe] }) export class AppComponent implements OnInit { public title = 'Tour of Heroes'; public heroes: Hero[]; public selectedHero: Hero; constructor(private _heroService: HeroService) { } onSelect(hero: Hero) { this.selectedHero = hero; } ngOnInit() { this.getHeroes() } getHeroes() { this._heroService.getHeroesSlowly().then(heros => this.heroes = heros); } }`, 'hero.ts': ` export interface Hero { id: number; name: string; }`, 'hero-detail.component.ts': ` import {Component, Input} from 'angular2/core'; import {Hero} from './hero'; @Component({ selector: 'my-hero-detail', template: ` + '`' + `

{{hero.name}} details!

{{hero.id}}
` + '`' + `, }) export class HeroDetailComponent { @Input() public hero: Hero; }`, 'mock-heroes.ts': ` import {Hero as Hero} from './hero'; export const HEROES: Hero[] = [ {"id": 11, "name": "Mr. Nice"}, {"id": 12, "name": "Narco"}, {"id": 13, "name": "Bombasto"}, {"id": 14, "name": "Celeritas"}, {"id": 15, "name": "Magneta"}, {"id": 16, "name": "RubberMan"}, {"id": 17, "name": "Dynama"}, {"id": 18, "name": "Dr IQ"}, {"id": 19, "name": "Magma"}, {"id": 20, "name": "Tornado"} ];`, 'default-exporter.ts': ` let a: string; export default a; `, 'hero.service.ts': ` import {Injectable} from 'angular2/core'; import {HEROES} from './mock-heroes'; import {Hero} from './hero'; @Injectable() class HeroService { getHeros() { return Promise.resolve(HEROES); } getHeroesSlowly() { return new Promise(resolve => setTimeout(()=>resolve(HEROES), 2000)); // 2 seconds } } export default HeroService;`, 'cases-data.ts': ` import {Injectable, Input} from 'angular2/core'; @Injectable() export class CaseAny { constructor(param: any) {} } @Injectable() export class GetProp { private _name: string; @Input('firstName') get name(): string { return this._name; } } @Injectable() export class SetProp { private _name: string; @Input('firstName') set name(value: string) { this._name = value; } } @Injectable() export class FullProp { private _name: string; @Input('firstName') get name(): string { return this._name; } set name(value: string) { this._name = value; } } export class ClassReference { } export class NgForRow { } @Injectable() export class NgFor { constructor (public ref: ClassReference) {} } `, 'error-cases.ts': ` import HeroService from './hero.service'; export class CaseCtor { constructor(private _heroService: HeroService) { } } ` }, 'promise.ts': ` interface PromiseLike { then(onfulfilled?: (value: T) => TResult | PromiseLike, onrejected?: (reason: any) => TResult | PromiseLike): PromiseLike; then(onfulfilled?: (value: T) => TResult | PromiseLike, onrejected?: (reason: any) => void): PromiseLike; } interface Promise { then(onfulfilled?: (value: T) => TResult | PromiseLike, onrejected?: (reason: any) => TResult | PromiseLike): Promise; then(onfulfilled?: (value: T) => TResult | PromiseLike, onrejected?: (reason: any) => void): Promise; catch(onrejected?: (reason: any) => T | PromiseLike): Promise; catch(onrejected?: (reason: any) => void): Promise; } interface PromiseConstructor { prototype: Promise; new (executor: (resolve: (value?: T | PromiseLike) => void, reject: (reason?: any) => void) => void): Promise; reject(reason: any): Promise; reject(reason: any): Promise; resolve(value: T | PromiseLike): Promise; resolve(): Promise; } declare var Promise: PromiseConstructor; `, 'unsupported-1.ts': ` export let {a, b} = {a: 1, b: 2}; export let [c, d] = [1, 2]; export let e; `, 'unsupported-2.ts': ` import {Injectable} from 'angular2/core'; class Foo {} @Injectable() export class Bar { constructor(private f: Foo) {} } `, 'node_modules': { 'angular2': { 'core.d.ts': ` export interface Type extends Function { } export interface TypeDecorator { (type: T): T; (target: Object, propertyKey?: string | symbol, parameterIndex?: number): void; annotations: any[]; } export interface ComponentDecorator extends TypeDecorator { } export interface ComponentFactory { (obj: { selector?: string; inputs?: string[]; outputs?: string[]; properties?: string[]; events?: string[]; host?: { [key: string]: string; }; bindings?: any[]; providers?: any[]; exportAs?: string; moduleId?: string; queries?: { [key: string]: any; }; viewBindings?: any[]; viewProviders?: any[]; templateUrl?: string; template?: string; styleUrls?: string[]; styles?: string[]; directives?: Array; pipes?: Array; }): ComponentDecorator; } export declare var Component: ComponentFactory; export interface InputFactory { (bindingPropertyName?: string): any; new (bindingPropertyName?: string): any; } export declare var Input: InputFactory; export interface InjectableFactory { (): any; } export declare var Injectable: InjectableFactory; export interface OnInit { ngOnInit(): any; } `, 'common.d.ts': ` export declare class NgFor { ngForOf: any; ngForTemplate: any; ngDoCheck(): void; } export declare class LowerCasePipe { transform(value: string, args?: any[]): string; } export declare class UpperCasePipe { transform(value: string, args?: any[]): string; } ` } } };