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 { 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 23:43:17 -05:00
const mockProject = { projectService : { logger : { info : function ( ) { } } } } ;
2016-11-22 12:10:23 -05: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 23:43:17 -05:00
let plugin = create (
{ ts : ts , languageService : service , project : mockProject , languageServiceHost : mockHost } ) ;
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' ) ) {
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' ,
( ) = > { 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
2017-07-07 19:55:17 -04:00
// it('should reference the element if no component', () => {
2016-11-22 12:10:23 -05:00
// 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' ) ;
} ) ;
2017-07-07 19:55:17 -04:00
it ( 'should report numeric operator errors' ,
2016-11-22 12:10:23 -05:00
( ) = > { 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 14:19:55 -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 {
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 (
locationMarker , plugin . getCompletionsAtPosition ( fileName , location , undefined ) , . . . names ) ;
2016-11-22 12:10:23 -05:00
}
function expectEmpty ( fileName : string , locationMarker : string ) {
const location = getMarkerLocation ( fileName , locationMarker ) ;
2017-12-22 12:36:47 -05:00
expect ( plugin . getCompletionsAtPosition ( fileName , location , undefined ) . entries || [ ] ) . toEqual ( [
] ) ;
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 ) ;
}