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' ;
2017-01-06 23:43:17 -05:00
import { create } from '../src/ts_plugin' ;
2016-11-22 12:10:23 -05:00
import { MockTypescriptHost } from './test_utils' ;
describe ( 'plugin' , ( ) = > {
2019-09-17 17:33:41 -04:00
const mockHost = new MockTypescriptHost ( [ '/app/main.ts' , '/app/parsing-cases.ts' ] ) ;
2019-07-31 13:55:45 -04:00
const service = ts . createLanguageService ( mockHost ) ;
const program = service . getProgram ( ) ;
const plugin = createPlugin ( service , mockHost ) ;
2017-01-06 23:43:17 -05:00
2016-11-22 12:10:23 -05:00
it ( 'should not report errors on tour of heroes' , ( ) = > {
expectNoDiagnostics ( service . getCompilerOptionsDiagnostics ( ) ) ;
2018-08-05 11:31:27 -04:00
for ( let source of program ! . getSourceFiles ( ) ) {
2016-11-22 12:10:23 -05:00
expectNoDiagnostics ( service . getSyntacticDiagnostics ( source . fileName ) ) ;
expectNoDiagnostics ( service . getSemanticDiagnostics ( source . fileName ) ) ;
}
} ) ;
it ( 'should not report template errors on tour of heroes' , ( ) = > {
2018-08-05 11:31:27 -04:00
for ( let source of program ! . getSourceFiles ( ) ) {
2016-11-22 12:10:23 -05:00
// Ignore all 'cases.ts' files as they intentionally contain errors.
if ( ! source . fileName . endsWith ( 'cases.ts' ) ) {
2017-01-06 23:43:17 -05:00
expectNoDiagnostics ( plugin . getSemanticDiagnostics ( source . fileName ) ) ;
2016-11-22 12:10:23 -05:00
}
}
} ) ;
it ( 'should be able to get entity completions' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/app.component.ts' , 'entity-amp' , '&' , '>' , '<' , 'ι' ) ; } ) ;
2016-11-22 12:10:23 -05:00
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 ) {
2019-09-12 18:20:54 -04:00
contains ( '/app/app.component.ts' , location , . . . htmlTags ) ;
2016-11-22 12:10:23 -05:00
}
} ) ;
2018-01-07 16:51:48 -05:00
it ( 'should be able to return element directives' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/app.component.ts' , 'empty' , 'my-app' ) ; } ) ;
2016-11-22 12:10:23 -05:00
2019-09-12 18:20:54 -04:00
it ( 'should be able to return h1 attributes' , ( ) = > {
contains ( '/app/app.component.ts' , 'h1-after-space' , 'id' , 'dir' , 'lang' , 'onclick' ) ;
} ) ;
2016-11-22 12:10:23 -05:00
it ( 'should be able to find common angular attributes' , ( ) = > {
2019-09-12 18:20:54 -04:00
contains ( '/app/app.component.ts' , 'div-attributes' , '(click)' , '[ngClass]' , '*ngIf' , '*ngFor' ) ;
2016-11-22 12:10:23 -05:00
} ) ;
2018-01-07 16:51:48 -05:00
it ( 'should be able to return attribute names with an incompete attribute' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/parsing-cases.ts' , 'no-value-attribute' , 'id' , 'dir' , 'lang' ) ; } ) ;
2016-11-22 12:10:23 -05:00
it ( 'should be able to return attributes of an incomplete element' , ( ) = > {
2019-09-12 18:20:54 -04:00
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' ) ;
2016-11-22 12:10:23 -05:00
} ) ;
it ( 'should be able to return completions with a missing closing tag' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/parsing-cases.ts' , 'missing-closing' , 'h1' , 'h2' ) ; } ) ;
2016-11-22 12:10:23 -05:00
2018-01-07 16:51:48 -05:00
it ( 'should be able to return common attributes of an unknown tag' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/parsing-cases.ts' , 'unknown-element' , 'id' , 'dir' , 'lang' ) ; } ) ;
2016-11-22 12:10:23 -05:00
it ( 'should be able to get the completions at the beginning of an interpolation' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/app.component.ts' , 'h2-hero' , 'hero' , 'title' ) ; } ) ;
2016-11-22 12:10:23 -05:00
2018-01-07 16:51:48 -05:00
it ( 'should not include private members of a class' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/app.component.ts' , 'h2-hero' , '-internal' ) ; } ) ;
2016-11-22 12:10:23 -05:00
it ( 'should be able to get the completions at the end of an interpolation' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/app.component.ts' , 'sub-end' , 'hero' , 'title' ) ; } ) ;
2016-11-22 12:10:23 -05:00
2018-01-07 16:51:48 -05:00
it ( 'should be able to get the completions in a property' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/app.component.ts' , 'h2-name' , 'name' , 'id' ) ; } ) ;
2016-11-22 12:10:23 -05:00
it ( 'should be able to get a list of pipe values' , ( ) = > {
2019-09-12 18:20:54 -04:00
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' ) ;
2016-11-22 12:10:23 -05:00
} ) ;
2018-01-07 16:51:48 -05:00
it ( 'should be able to get completions in an empty interpolation' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/parsing-cases.ts' , 'empty-interpolation' , 'title' , 'subTitle' ) ; } ) ;
2016-11-22 12:10:23 -05:00
describe ( 'with attributes' , ( ) = > {
it ( 'should be able to complete property value' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/parsing-cases.ts' , 'property-binding-model' , 'test' ) ; } ) ;
2016-11-22 12:10:23 -05:00
it ( 'should be able to complete an event' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/parsing-cases.ts' , 'event-binding-model' , 'modelChanged' ) ; } ) ;
2016-11-22 12:10:23 -05:00
it ( 'should be able to complete a two-way binding' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/parsing-cases.ts' , 'two-way-binding-model' , 'test' ) ; } ) ;
2016-11-22 12:10:23 -05:00
} ) ;
describe ( 'with a *ngFor' , ( ) = > {
it ( 'should include a let for empty attribute' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/parsing-cases.ts' , 'for-empty' , 'let' ) ; } ) ;
2016-11-22 12:10:23 -05:00
it ( 'should suggest NgForRow members for let initialization expression' , ( ) = > {
contains (
2019-09-12 18:20:54 -04:00
'/app/parsing-cases.ts' , 'for-let-i-equal' , 'index' , 'count' , 'first' , 'last' , 'even' ,
2016-11-22 12:10:23 -05:00
'odd' ) ;
} ) ;
2019-09-12 18:20:54 -04:00
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' ) ; } ) ;
2016-11-22 12:10:23 -05:00
it ( 'should include field reference' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/parsing-cases.ts' , 'for-people' , 'people' ) ; } ) ;
2016-11-22 12:10:23 -05:00
it ( 'should include person in the let scope' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/parsing-cases.ts' , 'for-interp-person' , 'person' ) ; } ) ;
2016-11-22 12:10:23 -05:00
// TODO: Enable when we can infer the element type of the ngFor
// it('should include determine person\'s type as Person', () => {
2019-09-12 18:20:54 -04:00
// contains('/app/parsing-cases.ts', 'for-interp-name', 'name', 'age');
// contains('/app/parsing-cases.ts', 'for-interp-age', 'name', 'age');
2016-11-22 12:10:23 -05:00
// });
} ) ;
describe ( 'for pipes' , ( ) = > {
it ( 'should be able to resolve lowercase' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/expression-cases.ts' , 'string-pipe' , 'substring' ) ; } ) ;
2016-11-22 12:10:23 -05:00
} ) ;
describe ( 'with references' , ( ) = > {
it ( 'should list references' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/parsing-cases.ts' , 'test-comp-content' , 'test1' , 'test2' , 'div' ) ; } ) ;
2016-11-22 12:10:23 -05:00
it ( 'should reference the component' ,
2019-09-12 18:20:54 -04:00
( ) = > { contains ( '/app/parsing-cases.ts' , 'test-comp-after-test' , 'name' ) ; } ) ;
2016-11-22 12:10:23 -05:00
// TODO: Enable when we have a flag that indicates the project targets the DOM
2017-07-07 19:55:17 -04:00
// it('should reference the element if no component', () => {
2019-09-12 18:20:54 -04:00
// contains('/app/parsing-cases.ts', 'test-comp-after-div', 'innerText');
2016-11-22 12:10:23 -05:00
// });
} ) ;
describe ( 'for semantic errors' , ( ) = > {
it ( 'should report access to an unknown field' , ( ) = > {
expectSemanticError (
2019-09-12 18:20:54 -04:00
'/app/expression-cases.ts' , 'foo' ,
2016-11-22 12:10:23 -05:00
'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 (
2019-09-12 18:20:54 -04:00
'/app/expression-cases.ts' , 'nam' ,
2016-11-22 12:10:23 -05:00
'Identifier \'nam\' is not defined. \'Person\' does not contain such a member' ) ;
} ) ;
it ( 'should report access to a private member' , ( ) = > {
expectSemanticError (
2019-09-12 18:20:54 -04:00
'/app/expression-cases.ts' , 'myField' ,
2016-11-22 12:10:23 -05:00
'Identifier \'myField\' refers to a private member of the component' ) ;
} ) ;
2019-09-12 18:20:54 -04:00
it ( 'should report numeric operator errors' , ( ) = > {
expectSemanticError ( '/app/expression-cases.ts' , 'mod' , 'Expected a numeric type' ) ;
} ) ;
2016-11-22 12:10:23 -05:00
describe ( 'in ngFor' , ( ) = > {
function expectError ( locationMarker : string , message : string ) {
2019-09-12 18:20:54 -04:00
expectSemanticError ( '/app/ng-for-cases.ts' , locationMarker , message ) ;
2016-11-22 12:10:23 -05:00
}
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' , ( ) = > {
2019-09-15 03:29:58 -04:00
expectError ( 'even_1' , 'The template context does not define a member called \'even_1\'' ) ;
2016-11-22 12:10:23 -05:00
} ) ;
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 ) {
2019-09-12 18:20:54 -04:00
expectSemanticError ( '/app/ng-if-cases.ts' , locationMarker , message ) ;
2016-11-22 12:10:23 -05:00
}
it ( 'should report an implicit context reference' , ( ) = > {
2019-09-15 03:29:58 -04:00
expectError ( 'implicit' , 'The template context does not define a member called \'unknown\'' ) ;
2016-11-22 12:10:23 -05:00
} ) ;
} ) ;
2019-07-31 13:55:45 -04:00
describe ( ` with config 'angularOnly = true ` , ( ) = > {
const ngLS = createPlugin ( service , mockHost , { angularOnly : true } ) ;
it ( 'should not report template errors on TOH' , ( ) = > {
const sourceFiles = ngLS . getProgram ( ) ! . getSourceFiles ( ) ;
expect ( sourceFiles . length ) . toBeGreaterThan ( 0 ) ;
for ( const { fileName } of sourceFiles ) {
// Ignore all 'cases.ts' files as they intentionally contain errors.
if ( ! fileName . endsWith ( 'cases.ts' ) ) {
expectNoDiagnostics ( ngLS . getSemanticDiagnostics ( fileName ) ) ;
}
}
} ) ;
it ( 'should be able to get entity completions' , ( ) = > {
2019-09-12 18:20:54 -04:00
const fileName = '/app/app.component.ts' ;
2019-07-31 13:55:45 -04:00
const marker = 'entity-amp' ;
const position = getMarkerLocation ( fileName , marker ) ;
const results = ngLS . getCompletionsAtPosition ( fileName , position , { } /* options */ ) ;
expect ( results ) . toBeTruthy ( ) ;
expectEntries ( marker , results ! , . . . [ '&' , '>' , '<' , 'ι' ] ) ;
} ) ;
it ( 'should report template diagnostics' , ( ) = > {
// TODO(kyliau): Rename these to end with '-error.ts'
2019-09-12 18:20:54 -04:00
const fileName = '/app/expression-cases.ts' ;
2019-07-31 13:55:45 -04:00
const diagnostics = ngLS . getSemanticDiagnostics ( fileName ) ;
expect ( diagnostics . map ( d = > d . messageText ) ) . toEqual ( [
` Identifier 'foo' is not defined. The component declaration, template variable declarations, and element references do not contain such a member ` ,
` Identifier 'nam' is not defined. 'Person' does not contain such a member ` ,
` Identifier 'myField' refers to a private member of the component ` ,
` Expected a numeric type ` ,
] ) ;
} ) ;
} ) ;
2016-11-22 12:10:23 -05:00
} ) ;
2019-07-31 13:55:45 -04:00
function createPlugin ( tsLS : ts.LanguageService , tsLSHost : ts.LanguageServiceHost , config = { } ) {
const project = { projectService : { logger : { info() { } } } } ;
return create ( {
languageService : tsLS ,
languageServiceHost : tsLSHost ,
project : project as any ,
serverHost : { } as any ,
config : { . . . config } ,
} ) ;
}
2016-11-22 12:10:23 -05:00
function getMarkerLocation ( fileName : string , locationMarker : string ) : number {
2017-03-24 12:57:32 -04:00
const location = mockHost . getMarkerLocations ( fileName ) ! [ locationMarker ] ;
2016-11-22 12:10:23 -05:00
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 ) ;
2017-12-22 12:36:47 -05:00
expectEntries (
2018-08-05 11:31:27 -04:00
locationMarker , plugin . getCompletionsAtPosition ( fileName , location , undefined ) ! , . . . names ) ;
2016-11-22 12:10:23 -05:00
}
function expectSemanticError ( fileName : string , locationMarker : string , message : string ) {
const start = getMarkerLocation ( fileName , locationMarker ) ;
const end = getMarkerLocation ( fileName , locationMarker + '-end' ) ;
2017-01-06 23:43:17 -05:00
const errors = plugin . getSemanticDiagnostics ( fileName ) ;
2016-11-22 12:10:23 -05: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 23:43:17 -05:00
throw new Error ( ` Expected error messages to contain ${ message } , in messages: \ n ${ errors
. map ( e = > e . messageText . toString ( ) )
. join ( ',\n ' ) } ` );
2016-11-22 12:10:23 -05:00
}
} ) ;
function expectEntries ( locationMarker : string , info : ts.CompletionInfo , . . . names : string [ ] ) {
let entries : { [ name : string ] : boolean } = { } ;
if ( ! info ) {
2017-01-06 23:43:17 -05:00
throw new Error ( ` Expected result from ${ locationMarker } to include ${ names . join (
', ' ) } but no result provided ` );
2016-11-22 12:10:23 -05: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 23:43:17 -05: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 12:10:23 -05:00
}
if ( present . length ) {
2017-01-06 23:43:17 -05:00
throw new Error ( ` Unexpected member ${ present . length > 1 ? 's' :
''
} included in result : $ { present . join ( ', ' ) } ` );
2016-11-22 12:10:23 -05:00
}
}
}
function expectNoDiagnostics ( diagnostics : ts.Diagnostic [ ] ) {
for ( const diagnostic of diagnostics ) {
let message = ts . flattenDiagnosticMessageText ( diagnostic . messageText , '\n' ) ;
2017-09-08 21:40:32 -04:00
if ( diagnostic . file && diagnostic . start ) {
2016-11-22 12:10:23 -05:00
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 ) ;
}