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 { createLanguageService } from '../src/language_service' ;
2017-01-03 20:21:45 -05:00
import { Completions } from '../src/types' ;
2016-11-22 12:10:23 -05:00
import { TypeScriptServiceHost } from '../src/typescript_host' ;
import { toh } from './test_data' ;
2017-01-03 20:21:45 -05:00
import { MockTypescriptHost } from './test_utils' ;
2016-11-22 12:10:23 -05:00
describe ( 'completions' , ( ) = > {
let documentRegistry = ts . createDocumentRegistry ( ) ;
let mockHost = new MockTypescriptHost ( [ '/app/main.ts' , '/app/parsing-cases.ts' ] , toh ) ;
let service = ts . createLanguageService ( mockHost , documentRegistry ) ;
2016-12-06 19:19:39 -05:00
let ngHost = new TypeScriptServiceHost ( mockHost , service ) ;
2016-11-22 12:10:23 -05:00
let ngService = createLanguageService ( ngHost ) ;
ngHost . setSite ( ngService ) ;
it ( 'should be able to get entity completions' ,
( ) = > { contains ( '/app/test.ng' , '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/test.ng' , location , . . . htmlTags ) ;
}
} ) ;
it ( 'should be able to return element diretives' ,
( ) = > { contains ( '/app/test.ng' , 'empty' , 'my-app' ) ; } ) ;
it ( 'should be able to return h1 attributes' ,
( ) = > { contains ( '/app/test.ng' , 'h1-after-space' , 'id' , 'dir' , 'lang' , 'onclick' ) ; } ) ;
it ( 'should be able to find common angular attributes' ,
( ) = > { contains ( '/app/test.ng' , 'div-attributes' , '(click)' , '[ngClass]' ) ; } ) ;
it ( 'should be able to get completions in some random garbage' , ( ) = > {
const fileName = '/app/test.ng' ;
mockHost . override ( fileName , ' > {{tle<\n {{retl ><bel/beled}}di>\n la</b </d &a ' ) ;
expect ( ( ) = > ngService . getCompletionsAt ( fileName , 31 ) ) . not . toThrow ( ) ;
2017-03-24 12:57:32 -04:00
mockHost . override ( fileName , undefined ! ) ;
2016-11-22 12:10:23 -05:00
} ) ;
it ( 'should be able to infer the type of a ngForOf' , ( ) = > {
addCode (
`
interface Person {
name : string ,
street : string
}
@Component ( { template : '<div *ngFor="let person of people">{{person.~{name}name}}</div' } )
export class MyComponent {
people : Person [ ]
} ` ,
( ) = > { contains ( '/app/app.component.ts' , 'name' , 'name' , 'street' ) ; } ) ;
} ) ;
it ( 'should be able to infer the type of a ngForOf with an async pipe' , ( ) = > {
addCode (
`
interface Person {
name : string ,
street : string
}
@Component ( { template : '<div *ngFor="let person of people | async">{{person.~{name}name}}</div' } )
export class MyComponent {
people : Promise < Person [ ] > ;
} ` ,
( ) = > { contains ( '/app/app.component.ts' , 'name' , 'name' , 'street' ) ; } ) ;
} ) ;
it ( 'should be able to complete every character in the file' , ( ) = > {
const fileName = '/app/test.ng' ;
expect ( ( ) = > {
let chance = 0.05 ;
let requests = 0 ;
function tryCompletionsAt ( position : number ) {
try {
if ( Math . random ( ) < chance ) {
ngService . getCompletionsAt ( fileName , position ) ;
requests ++ ;
}
} catch ( e ) {
// Emit enough diagnostic information to reproduce the error.
2016-11-23 19:21:06 -05:00
console . error (
2016-11-22 12:10:23 -05:00
` Position: ${ position } \ nContent: " ${ mockHost . getFileContent ( fileName ) } " \ nStack: \ n ${ e . stack } ` ) ;
throw e ;
}
}
try {
2017-03-24 12:57:32 -04:00
const originalContent = mockHost . getFileContent ( fileName ) ! ;
2016-11-22 12:10:23 -05:00
// For each character in the file, add it to the file and request a completion after it.
for ( let index = 0 , len = originalContent . length ; index < len ; index ++ ) {
const content = originalContent . substr ( 0 , index ) ;
mockHost . override ( fileName , content ) ;
tryCompletionsAt ( index ) ;
}
// For the complete file, try to get a completion at every character.
mockHost . override ( fileName , originalContent ) ;
for ( let index = 0 , len = originalContent . length ; index < len ; index ++ ) {
tryCompletionsAt ( index ) ;
}
// Delete random characters in the file until we get an empty file.
let content = originalContent ;
while ( content . length > 0 ) {
const deleteIndex = Math . floor ( Math . random ( ) * content . length ) ;
content = content . slice ( 0 , deleteIndex - 1 ) + content . slice ( deleteIndex + 1 ) ;
mockHost . override ( fileName , content ) ;
const requestIndex = Math . floor ( Math . random ( ) * content . length ) ;
tryCompletionsAt ( requestIndex ) ;
}
// Build up the string from zero asking for a completion after every char
buildUp ( originalContent , ( text , position ) = > {
mockHost . override ( fileName , text ) ;
tryCompletionsAt ( position ) ;
} ) ;
} finally {
2017-03-24 12:57:32 -04:00
mockHost . override ( fileName , undefined ! ) ;
2016-11-22 12:10:23 -05:00
}
} ) . not . toThrow ( ) ;
} ) ;
describe ( 'with regression tests' , ( ) = > {
it ( 'should not crash with an incomplete component' , ( ) = > {
expect ( ( ) = > {
const code = `
@Component ( {
template : '~{inside-template}'
} )
export class MyComponent {
} ` ;
addCode ( code , fileName = > { contains ( fileName , 'inside-template' , 'h1' ) ; } ) ;
} ) . not . toThrow ( ) ;
} ) ;
it ( 'should hot crash with an incomplete class' , ( ) = > {
expect ( ( ) = > {
addCode ( '\nexport class' , fileName = > { ngHost . updateAnalyzedModules ( ) ; } ) ;
} ) . not . toThrow ( ) ;
} ) ;
} ) ;
2017-11-06 13:30:29 -05:00
it ( 'should respect paths configuration' , ( ) = > {
mockHost . overrideOptions ( options = > {
options . baseUrl = '/app' ;
options . paths = { 'bar/*' : [ 'foo/bar/*' ] } ;
return options ;
} ) ;
mockHost . addScript ( '/app/foo/bar/shared.ts' , `
export interface Node {
children : Node [ ] ;
}
` );
mockHost . addScript ( '/app/my.component.ts' , `
import { Component } from '@angular/core' ;
import { Node } from 'bar/shared' ;
@Component ( {
selector : 'my-component' ,
template : '{{tree.~{tree} }}'
} )
export class MyComponent {
tree : Node ;
}
` );
ngHost . updateAnalyzedModules ( ) ;
contains ( '/app/my.component.ts' , 'tree' , 'children' ) ;
} ) ;
2019-01-30 13:34:36 -05:00
it ( 'should work with input and output' , ( ) = > {
addCode (
`
@Component ( {
selector : 'foo-component' ,
template : \ `
< div string - model ~ { stringMarker } = " text " > < / div >
< div number - model ~ { numberMarker } = " value " > < / div >
\ ` ,
} )
export class FooComponent {
text : string ;
value : number ;
}
` ,
( fileName ) = > {
contains ( fileName , 'stringMarker' , '[model]' , '(model)' ) ;
contains ( fileName , 'numberMarker' , '[inputAlias]' , '(outputAlias)' ) ;
} ) ;
} ) ;
2016-11-22 12:10:23 -05:00
function addCode ( code : string , cb : ( fileName : string , content? : string ) = > void ) {
const fileName = '/app/app.component.ts' ;
const originalContent = mockHost . getFileContent ( fileName ) ;
const newContent = originalContent + code ;
mockHost . override ( fileName , originalContent + code ) ;
2016-12-02 17:34:16 -05:00
ngHost . updateAnalyzedModules ( ) ;
2016-11-22 12:10:23 -05:00
try {
cb ( fileName , newContent ) ;
} finally {
2017-03-24 12:57:32 -04:00
mockHost . override ( fileName , undefined ! ) ;
2016-11-22 12:10:23 -05:00
}
}
function contains ( fileName : string , locationMarker : string , . . . names : string [ ] ) {
2017-03-24 12:57:32 -04:00
let location = mockHost . getMarkerLocations ( fileName ) ! [ locationMarker ] ;
2016-11-22 12:10:23 -05:00
if ( location == null ) {
throw new Error ( ` No marker ${ locationMarker } found. ` ) ;
}
expectEntries ( locationMarker , ngService . getCompletionsAt ( fileName , location ) , . . . names ) ;
}
} ) ;
function expectEntries ( locationMarker : string , completions : Completions , . . . names : string [ ] ) {
let entries : { [ name : string ] : boolean } = { } ;
if ( ! completions ) {
throw new Error (
` Expected result from ${ locationMarker } to include ${ names . join ( ', ' ) } but no result provided ` ) ;
}
if ( ! completions . length ) {
throw new Error (
` Expected result from ${ locationMarker } to include ${ names . join ( ', ' ) } an empty result provided ` ) ;
} else {
for ( let entry of completions ) {
entries [ entry . name ] = true ;
}
let missing = names . 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 ${ completions . map ( entry = > entry . name ) . join ( ', ' ) } ` ) ;
}
}
}
function buildUp ( originalText : string , cb : ( text : string , position : number ) = > void ) {
let count = originalText . length ;
let inString : boolean [ ] = ( new Array ( count ) ) . fill ( false ) ;
let unused : number [ ] = ( new Array ( count ) ) . fill ( 1 ) . map ( ( v , i ) = > i ) ;
function getText() {
return new Array ( count )
. fill ( 1 )
. map ( ( v , i ) = > i )
. filter ( i = > inString [ i ] )
. map ( i = > originalText [ i ] )
. join ( '' ) ;
}
function randomUnusedIndex() { return Math . floor ( Math . random ( ) * unused . length ) ; }
while ( unused . length > 0 ) {
let unusedIndex = randomUnusedIndex ( ) ;
let index = unused [ unusedIndex ] ;
if ( index == null ) throw new Error ( 'Internal test buildup error' ) ;
if ( inString [ index ] ) throw new Error ( 'Internal test buildup error' ) ;
inString [ index ] = true ;
unused . splice ( unusedIndex , 1 ) ;
let text = getText ( ) ;
let position =
inString . filter ( ( _ , i ) = > i <= index ) . map ( v = > v ? 1 : 0 ) . reduce ( ( p , v ) = > p + v , 0 ) ;
cb ( text , position ) ;
}
}