2016-11-22 12:10:23 -05: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' ;
import { LanguageServicePlugin } from '../src/ts_plugin' ;
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 ( ) ;
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 ) ) ;
}
} ) ;
2016-12-06 19:19:39 -05:00
let plugin = new LanguageServicePlugin ( { host : mockHost , service , registry : documentRegistry } ) ;
2016-11-22 12:10:23 -05: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' ) ) {
expectNoDiagnostics ( plugin . getSemanticDiagnosticsFilter ( source . fileName , [ ] ) ) ;
}
}
} ) ;
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 not suggest any entries if in the name part of a let' ,
( ) = > { expectEmpty ( 'app/parsing-cases.ts' , 'for-let-empty' ) ; } ) ;
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-08 00:41:27 -05:00
expectError ( 'implicit' , "The template context does not defined a member called 'unknown'" ) ;
2016-11-22 12:10:23 -05: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 ) ;
expect ( plugin . getCompletionsAtPosition ( fileName , location ) . entries ) . toEqual ( [ ] ) ;
}
function expectSemanticError ( fileName : string , locationMarker : string , message : string ) {
const start = getMarkerLocation ( fileName , locationMarker ) ;
const end = getMarkerLocation ( fileName , locationMarker + '-end' ) ;
const errors = plugin . getSemanticDiagnosticsFilter ( fileName , [ ] ) ;
for ( const error of errors ) {
if ( error . messageText . toString ( ) . indexOf ( message ) >= 0 ) {
expect ( error . start ) . toEqual ( start ) ;
expect ( error . length ) . toEqual ( end - start ) ;
return ;
}
}
throw new Error (
` Expected error messages to contain ${ message } , in messages: \ n ${ errors . map ( e = > e . messageText . toString ( ) ) . join ( ',\n ' ) } ` ) ;
}
} ) ;
function expectEntries ( locationMarker : string , info : ts.CompletionInfo , . . . names : string [ ] ) {
let entries : { [ name : string ] : boolean } = { } ;
if ( ! info ) {
throw new Error (
` Expected result from ${ locationMarker } to include ${ names . join ( ', ' ) } but no result provided ` ) ;
} 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 ) {
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 ( ', ' ) } ` ) ;
}
if ( present . length ) {
throw new Error (
` Unexpected member ${ present . length > 1 ? 's' : '' } included in result: ${ present . join ( ', ' ) } ` ) ;
}
}
}
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 19:21:06 -05:00
console . error ( ` ${ diagnostic . file . fileName } ( ${ line + 1 } , ${ character + 1 } ): ${ message } ` ) ;
2016-11-22 12:10:23 -05:00
} else {
2016-11-23 19:21:06 -05:00
console . error ( ` ${ message } ` ) ;
2016-11-22 12:10:23 -05:00
}
}
expect ( diagnostics . length ) . toBe ( 0 ) ;
}