2016-11-22 09:10:23 -08:00
/**
* @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 'reflect-metadata' ;
import * as ts from 'typescript' ;
2017-01-06 20:43:17 -08:00
import { create } from '../src/ts_plugin' ;
2016-11-22 09:10:23 -08:00
import { toh } from './test_data' ;
import { MockTypescriptHost } from './test_utils' ;
describe ( 'plugin' , () => {
let documentRegistry = ts . createDocumentRegistry ();
let mockHost = new MockTypescriptHost ([ '/app/main.ts' , '/app/parsing-cases.ts' ], toh );
let service = ts . createLanguageService ( mockHost , documentRegistry );
let program = service . getProgram ();
2017-01-06 20:43:17 -08:00
const mockProject = { projectService : { logger : { info : function () {}}}};
2016-11-22 09:10:23 -08:00
it ( 'should not report errors on tour of heroes' , () => {
expectNoDiagnostics ( service . getCompilerOptionsDiagnostics ());
for ( let source of program . getSourceFiles ()) {
expectNoDiagnostics ( service . getSyntacticDiagnostics ( source . fileName ));
expectNoDiagnostics ( service . getSemanticDiagnostics ( source . fileName ));
}
});
2017-01-06 20:43:17 -08:00
let plugin = create (
{ ts : ts , languageService : service , project : mockProject , languageServiceHost : mockHost });
2016-11-22 09:10:23 -08:00
it ( 'should not report template errors on tour of heroes' , () => {
for ( let source of program . getSourceFiles ()) {
// Ignore all 'cases.ts' files as they intentionally contain errors.
if ( ! source . fileName . endsWith ( 'cases.ts' )) {
2017-01-06 20:43:17 -08:00
expectNoDiagnostics ( plugin . getSemanticDiagnostics ( source . fileName ));
2016-11-22 09:10:23 -08:00
}
}
});
it ( 'should be able to get entity completions' ,
() => { contains ( 'app/app.component.ts' , 'entity-amp' , '&' , '>' , '<' , 'ι' ); });
it ( 'should be able to return html elements' , () => {
let htmlTags = [ 'h1' , 'h2' , 'div' , 'span' ];
let locations = [ 'empty' , 'start-tag-h1' , 'h1-content' , 'start-tag' , 'start-tag-after-h' ];
for ( let location of locations ) {
contains ( 'app/app.component.ts' , location , ... htmlTags );
}
});
it ( 'should be able to return element diretives' ,
() => { contains ( 'app/app.component.ts' , 'empty' , 'my-app' ); });
it ( 'should be able to return h1 attributes' ,
() => { contains ( 'app/app.component.ts' , 'h1-after-space' , 'id' , 'dir' , 'lang' , 'onclick' ); });
it ( 'should be able to find common angular attributes' , () => {
contains ( 'app/app.component.ts' , 'div-attributes' , '(click)' , '[ngClass]' , '*ngIf' , '*ngFor' );
});
it ( 'should be able to returned attribute names with an incompete attribute' ,
() => { contains ( 'app/parsing-cases.ts' , 'no-value-attribute' , 'id' , 'dir' , 'lang' ); });
it ( 'should be able to return attributes of an incomplete element' , () => {
contains ( 'app/parsing-cases.ts' , 'incomplete-open-lt' , 'a' );
contains ( 'app/parsing-cases.ts' , 'incomplete-open-a' , 'a' );
contains ( 'app/parsing-cases.ts' , 'incomplete-open-attr' , 'id' , 'dir' , 'lang' );
});
it ( 'should be able to return completions with a missing closing tag' ,
() => { contains ( 'app/parsing-cases.ts' , 'missing-closing' , 'h1' , 'h2' ); });
it ( 'should be able to return common attributes of in an unknown tag' ,
() => { contains ( 'app/parsing-cases.ts' , 'unknown-element' , 'id' , 'dir' , 'lang' ); });
it ( 'should be able to get the completions at the beginning of an interpolation' ,
() => { contains ( 'app/app.component.ts' , 'h2-hero' , 'hero' , 'title' ); });
it ( 'should not include private members of the of a class' ,
() => { contains ( 'app/app.component.ts' , 'h2-hero' , '-internal' ); });
it ( 'should be able to get the completions at the end of an interpolation' ,
() => { contains ( 'app/app.component.ts' , 'sub-end' , 'hero' , 'title' ); });
it ( 'should be able to get the completions in a property read' ,
() => { contains ( 'app/app.component.ts' , 'h2-name' , 'name' , 'id' ); });
it ( 'should be able to get a list of pipe values' , () => {
contains ( 'app/parsing-cases.ts' , 'before-pipe' , 'lowercase' , 'uppercase' );
contains ( 'app/parsing-cases.ts' , 'in-pipe' , 'lowercase' , 'uppercase' );
contains ( 'app/parsing-cases.ts' , 'after-pipe' , 'lowercase' , 'uppercase' );
});
it ( 'should be able get completions in an empty interpolation' ,
() => { contains ( 'app/parsing-cases.ts' , 'empty-interpolation' , 'title' , 'subTitle' ); });
describe ( 'with attributes' , () => {
it ( 'should be able to complete property value' ,
() => { contains ( 'app/parsing-cases.ts' , 'property-binding-model' , 'test' ); });
it ( 'should be able to complete an event' ,
() => { contains ( 'app/parsing-cases.ts' , 'event-binding-model' , 'modelChanged' ); });
it ( 'should be able to complete a two-way binding' ,
() => { contains ( 'app/parsing-cases.ts' , 'two-way-binding-model' , 'test' ); });
});
describe ( 'with a *ngFor' , () => {
it ( 'should include a let for empty attribute' ,
() => { contains ( 'app/parsing-cases.ts' , 'for-empty' , 'let' ); });
it ( 'should suggest NgForRow members for let initialization expression' , () => {
contains (
'app/parsing-cases.ts' , 'for-let-i-equal' , 'index' , 'count' , 'first' , 'last' , 'even' ,
'odd' );
});
it ( 'should include a let' , () => { contains ( 'app/parsing-cases.ts' , 'for-let' , 'let' ); });
it ( 'should include an "of"' , () => { contains ( 'app/parsing-cases.ts' , 'for-of' , 'of' ); });
it ( 'should include field reference' ,
() => { contains ( 'app/parsing-cases.ts' , 'for-people' , 'people' ); });
it ( 'should include person in the let scope' ,
() => { contains ( 'app/parsing-cases.ts' , 'for-interp-person' , 'person' ); });
// TODO: Enable when we can infer the element type of the ngFor
// it('should include determine person\'s type as Person', () => {
// contains('app/parsing-cases.ts', 'for-interp-name', 'name', 'age');
// contains('app/parsing-cases.ts', 'for-interp-age', 'name', 'age');
// });
});
describe ( 'for pipes' , () => {
it ( 'should be able to resolve lowercase' ,
() => { contains ( 'app/expression-cases.ts' , 'string-pipe' , 'substring' ); });
});
describe ( 'with references' , () => {
it ( 'should list references' ,
() => { contains ( 'app/parsing-cases.ts' , 'test-comp-content' , 'test1' , 'test2' , 'div' ); });
it ( 'should reference the component' ,
() => { contains ( 'app/parsing-cases.ts' , 'test-comp-after-test' , 'name' ); });
// TODO: Enable when we have a flag that indicates the project targets the DOM
// it('should refernce the element if no component', () => {
// contains('app/parsing-cases.ts', 'test-comp-after-div', 'innerText');
// });
});
describe ( 'for semantic errors' , () => {
it ( 'should report access to an unknown field' , () => {
expectSemanticError (
'app/expression-cases.ts' , 'foo' ,
'Identifier \'foo\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member' );
});
it ( 'should report access to an unknown sub-field' , () => {
expectSemanticError (
'app/expression-cases.ts' , 'nam' ,
'Identifier \'nam\' is not defined. \'Person\' does not contain such a member' );
});
it ( 'should report access to a private member' , () => {
expectSemanticError (
'app/expression-cases.ts' , 'myField' ,
'Identifier \'myField\' refers to a private member of the component' );
});
it ( 'should report numeric operator erros' ,
() => { expectSemanticError ( 'app/expression-cases.ts' , 'mod' , 'Expected a numeric type' ); });
describe ( 'in ngFor' , () => {
function expectError ( locationMarker : string , message : string ) {
expectSemanticError ( 'app/ng-for-cases.ts' , locationMarker , message );
}
it ( 'should report an unknown field' , () => {
expectError (
'people_1' ,
'Identifier \'people_1\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member' );
});
it ( 'should report an unknown context reference' , () => {
expectError ( 'even_1' , 'The template context does not defined a member called \'even_1\'' );
});
it ( 'should report an unknown value in a key expression' , () => {
expectError (
'trackBy_1' ,
'Identifier \'trackBy_1\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member' );
});
});
describe ( 'in ngIf' , () => {
function expectError ( locationMarker : string , message : string ) {
expectSemanticError ( 'app/ng-if-cases.ts' , locationMarker , message );
}
it ( 'should report an implicit context reference' , () => {
2016-12-09 11:19:55 -08:00
expectError (
'implicit' , 'The template context does not defined a member called \'unknown\'' );
2016-11-22 09:10:23 -08:00
});
});
});
function getMarkerLocation ( fileName : string , locationMarker : string ) : number {
const location = mockHost . getMarkerLocations ( fileName )[ locationMarker ];
if ( location == null ) {
throw new Error ( `No marker ${ locationMarker } found.` );
}
return location ;
}
function contains ( fileName : string , locationMarker : string , ... names : string []) {
const location = getMarkerLocation ( fileName , locationMarker );
expectEntries ( locationMarker , plugin . getCompletionsAtPosition ( fileName , location ), ... names );
}
function expectEmpty ( fileName : string , locationMarker : string ) {
const location = getMarkerLocation ( fileName , locationMarker );
2017-01-06 20:43:17 -08:00
expect ( plugin . getCompletionsAtPosition ( fileName , location ). entries || []). toEqual ([]);
2016-11-22 09:10:23 -08:00
}
function expectSemanticError ( fileName : string , locationMarker : string , message : string ) {
const start = getMarkerLocation ( fileName , locationMarker );
const end = getMarkerLocation ( fileName , locationMarker + '-end' );
2017-01-06 20:43:17 -08:00
const errors = plugin . getSemanticDiagnostics ( fileName );
2016-11-22 09:10:23 -08:00
for ( const error of errors ) {
if ( error . messageText . toString (). indexOf ( message ) >= 0 ) {
expect ( error . start ). toEqual ( start );
expect ( error . length ). toEqual ( end - start );
return ;
}
}
2017-01-06 20:43:17 -08:00
throw new Error ( `Expected error messages to contain ${ message } , in messages: \ n ${ errors
. map ( e => e . messageText . toString ())
. join ( ',\n ' ) } ` );
2016-11-22 09:10:23 -08:00
}
});
function expectEntries ( locationMarker : string , info : ts.CompletionInfo , ... names : string []) {
let entries : {[ name : string ] : boolean } = {};
if ( ! info ) {
2017-01-06 20:43:17 -08:00
throw new Error ( `Expected result from ${ locationMarker } to include ${ names . join (
', ' ) } but no result provided` );
2016-11-22 09:10:23 -08:00
} else {
for ( let entry of info . entries ) {
entries [ entry . name ] = true ;
}
let shouldContains = names . filter ( name => ! name . startsWith ( '-' ));
let shouldNotContain = names . filter ( name => name . startsWith ( '-' ));
let missing = shouldContains . filter ( name => ! entries [ name ]);
let present = shouldNotContain . map ( name => name . substr ( 1 )). filter ( name => entries [ name ]);
if ( missing . length ) {
2017-01-06 20:43:17 -08:00
throw new Error ( `Expected result from ${ locationMarker
} to include at least one of the following, ${ missing
. join ( ', ' ) } , in the list of entries ${ info . entries . map ( entry => entry . name )
. join ( ', ' ) } ` );
2016-11-22 09:10:23 -08:00
}
if ( present . length ) {
2017-01-06 20:43:17 -08:00
throw new Error ( `Unexpected member ${ present . length > 1 ? 's' :
''
} included in result: ${ present . join ( ', ' ) } ` );
2016-11-22 09:10:23 -08:00
}
}
}
function expectNoDiagnostics ( diagnostics : ts.Diagnostic []) {
for ( const diagnostic of diagnostics ) {
let message = ts . flattenDiagnosticMessageText ( diagnostic . messageText , '\n' );
if ( diagnostic . start ) {
let { line , character } = diagnostic . file . getLineAndCharacterOfPosition ( diagnostic . start );
2016-11-23 16:21:06 -08:00
console . error ( ` ${ diagnostic . file . fileName } ( ${ line + 1 } , ${ character + 1 } ): ${ message } ` );
2016-11-22 09:10:23 -08:00
} else {
2016-11-23 16:21:06 -08:00
console . error ( ` ${ message } ` );
2016-11-22 09:10:23 -08:00
}
}
expect ( diagnostics . length ). toBe ( 0 );
}