2017-05-09 19:16:50 -04:00
/ * *
* @license
2020-05-19 15:08:49 -04:00
* Copyright Google LLC All Rights Reserved .
2017-05-09 19:16:50 -04:00
*
* 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 { StaticSymbol } from '@angular/compiler' ;
2019-11-13 17:26:58 -05:00
import { Directory } from '@angular/compiler-cli/test/mocks' ;
2017-10-20 12:46:41 -04:00
import { ReflectorHost } from '@angular/language-service/src/reflector_host' ;
2017-05-09 19:16:50 -04:00
import * as ts from 'typescript' ;
2019-11-13 17:26:58 -05:00
import { getTemplateExpressionDiagnostics } from '../src/expression_diagnostics' ;
2017-05-09 19:16:50 -04:00
2020-04-03 23:57:39 -04:00
import { DiagnosticContext , getDiagnosticTemplateInfo , MockLanguageServiceHost } from './mocks' ;
2017-05-09 19:16:50 -04:00
describe ( 'expression diagnostics' , ( ) = > {
let registry : ts.DocumentRegistry ;
2018-03-21 17:22:06 -04:00
2017-05-09 19:16:50 -04:00
let host : MockLanguageServiceHost ;
let service : ts.LanguageService ;
let context : DiagnosticContext ;
let type : StaticSymbol ;
beforeAll ( ( ) = > {
registry = ts . createDocumentRegistry ( false , '/src' ) ;
host = new MockLanguageServiceHost ( [ 'app/app.component.ts' ] , FILES , '/src' ) ;
service = ts . createLanguageService ( host , registry ) ;
2020-04-03 23:57:39 -04:00
const program = service . getProgram ( ) ! ;
2017-05-09 19:16:50 -04:00
const checker = program . getTypeChecker ( ) ;
2020-04-03 23:57:39 -04:00
const symbolResolverHost = new ReflectorHost ( ( ) = > program ! , host ) ;
context = new DiagnosticContext ( service , program ! , checker , symbolResolverHost ) ;
2017-05-09 19:16:50 -04:00
type = context . getStaticSymbol ( 'app/app.component.ts' , 'AppComponent' ) ;
} ) ;
it ( 'should have no diagnostics in default app' , ( ) = > {
2020-04-03 23:57:39 -04:00
function messageToString ( messageText : string | ts . DiagnosticMessageChain ) : string {
2017-05-09 19:16:50 -04:00
if ( typeof messageText == 'string' ) {
return messageText ;
} else {
2019-10-01 19:44:50 -04:00
if ( messageText . next )
return messageText . messageText + messageText . next . map ( messageToString ) ;
2017-05-09 19:16:50 -04:00
return messageText . messageText ;
}
}
function expectNoDiagnostics ( diagnostics : ts.Diagnostic [ ] ) {
if ( diagnostics && diagnostics . length ) {
const message =
2018-03-10 12:14:58 -05:00
'messages: ' + diagnostics . map ( d = > messageToString ( d . messageText ) ) . join ( '\n' ) ;
2017-05-09 19:16:50 -04:00
expect ( message ) . toEqual ( '' ) ;
}
}
expectNoDiagnostics ( service . getCompilerOptionsDiagnostics ( ) ) ;
expectNoDiagnostics ( service . getSyntacticDiagnostics ( 'app/app.component.ts' ) ) ;
expectNoDiagnostics ( service . getSemanticDiagnostics ( 'app/app.component.ts' ) ) ;
} ) ;
function accept ( template : string ) {
const info = getDiagnosticTemplateInfo ( context , type , 'app/app.component.html' , template ) ;
if ( info ) {
const diagnostics = getTemplateExpressionDiagnostics ( info ) ;
if ( diagnostics && diagnostics . length ) {
const message = diagnostics . map ( d = > d . message ) . join ( '\n ' ) ;
throw new Error ( ` Unexpected diagnostics: ${ message } ` ) ;
}
} else {
expect ( info ) . toBeDefined ( ) ;
}
}
2017-10-24 07:54:08 -04:00
function reject ( template : string , expected : string ) {
2017-05-09 19:16:50 -04:00
const info = getDiagnosticTemplateInfo ( context , type , 'app/app.component.html' , template ) ;
if ( info ) {
const diagnostics = getTemplateExpressionDiagnostics ( info ) ;
if ( diagnostics && diagnostics . length ) {
const messages = diagnostics . map ( d = > d . message ) . join ( '\n ' ) ;
expect ( messages ) . toContain ( expected ) ;
} else {
throw new Error ( ` Expected an error containing " ${ expected } in template " ${ template } " ` ) ;
}
} else {
expect ( info ) . toBeDefined ( ) ;
}
}
it ( 'should accept a simple template' , ( ) = > accept ( 'App works!' ) ) ;
it ( 'should accept an interpolation' , ( ) = > accept ( 'App works: {{person.name.first}}' ) ) ;
it ( 'should reject misspelled access' ,
( ) = > reject ( '{{persson}}' , 'Identifier \'persson\' is not defined' ) ) ;
it ( 'should reject access to private' ,
( ) = >
reject ( '{{private_person}}' , 'Identifier \'private_person\' refers to a private member' ) ) ;
it ( 'should accept an *ngIf' , ( ) = > accept ( '<div *ngIf="person">{{person.name.first}}</div>' ) ) ;
it ( 'should reject *ngIf of misspelled identifier' ,
( ) = > reject (
'<div *ngIf="persson">{{person.name.first}}</div>' ,
'Identifier \'persson\' is not defined' ) ) ;
2019-10-10 12:07:48 -04:00
it ( 'should reject *ngIf of misspelled identifier in PrefixNot node' ,
( ) = >
reject ( '<div *ngIf="people && !persson"></div>' , 'Identifier \'persson\' is not defined' ) ) ;
2020-07-03 19:52:40 -04:00
it ( 'should reject misspelled field in unary operator expression' ,
( ) = > reject ( '{{ +persson }}' , ` Identifier 'persson' is not defined ` ) ) ;
2017-05-09 19:16:50 -04:00
it ( 'should accept an *ngFor' , ( ) = > accept ( `
< div * ngFor = "let p of people" >
{ { p . name . first } } { { p . name . last } }
< / div >
` ));
2020-04-03 23:57:39 -04:00
it ( 'should reject misspelled field in *ngFor' ,
( ) = > reject (
`
2017-05-09 19:16:50 -04:00
< div * ngFor = "let p of people" >
{ { p . names . first } } { { p . name . last } }
< / div >
` ,
2020-04-03 23:57:39 -04:00
'Identifier \'names\' is not defined' ) ) ;
2017-05-09 19:16:50 -04:00
it ( 'should accept an async expression' ,
( ) = > accept ( '{{(promised_person | async)?.name.first || ""}}' ) ) ;
it ( 'should reject an async misspelled field' ,
( ) = > reject (
'{{(promised_person | async)?.nume.first || ""}}' , 'Identifier \'nume\' is not defined' ) ) ;
it ( 'should accept an async *ngFor' , ( ) = > accept ( `
< div * ngFor = "let p of promised_people | async" >
{ { p . name . first } } { { p . name . last } }
< / div >
` ));
2020-04-03 23:57:39 -04:00
it ( 'should reject misspelled field an async *ngFor' ,
( ) = > reject (
`
2017-05-09 19:16:50 -04:00
< div * ngFor = "let p of promised_people | async" >
{ { p . name . first } } { { p . nume . last } }
< / div >
` ,
2020-04-03 23:57:39 -04:00
'Identifier \'nume\' is not defined' ) ) ;
2019-10-05 08:18:05 -04:00
it ( 'should accept an async *ngIf' , ( ) = > accept ( `
< div * ngIf = "promised_person | async as p" >
{ { p . name . first } } { { p . name . last } }
< / div >
` ));
2020-04-03 23:57:39 -04:00
it ( 'should reject misspelled field in async *ngIf' ,
( ) = > reject (
`
2019-10-05 08:18:05 -04:00
< div * ngIf = "promised_person | async as p" >
{ { p . name . first } } { { p . nume . last } }
< / div >
` ,
2020-04-03 23:57:39 -04:00
'Identifier \'nume\' is not defined' ) ) ;
2017-05-09 19:16:50 -04:00
it ( 'should reject access to potentially undefined field' ,
2020-02-06 17:49:49 -05:00
( ) = > reject (
` <div>{{maybe_person.name.first}} ` ,
` 'maybe_person' is possibly undefined. Consider using the safe navigation operator (maybe_person?.name) or non-null assertion operator (maybe_person!.name). ` ) ) ;
2017-05-09 19:16:50 -04:00
it ( 'should accept a safe accss to an undefined field' ,
( ) = > accept ( ` <div>{{maybe_person?.name.first}}</div> ` ) ) ;
2017-05-11 13:15:54 -04:00
it ( 'should accept a type assert to an undefined field' ,
( ) = > accept ( ` <div>{{maybe_person!.name.first}}</div> ` ) ) ;
2017-05-09 19:16:50 -04:00
it ( 'should accept a # reference' , ( ) = > accept ( `
< form # f = "ngForm" novalidate >
< input name = "first" ngModel required # first = "ngModel" >
< input name = "last" ngModel >
< button > Submit < / button >
< / form >
< p > First name value : { { first . value } } < / p >
< p > First name valid : { { first . valid } } < / p >
< p > Form value : { { f . value | json } } < / p >
< p > Form valid : { { f . valid } } < / p >
` ));
it ( 'should reject a misspelled field of a # reference' ,
( ) = > reject (
`
< form # f = "ngForm" novalidate >
< input name = "first" ngModel required # first = "ngModel" >
< input name = "last" ngModel >
< button > Submit < / button >
< / form >
< p > First name value : { { first . valwe } } < / p >
< p > First name valid : { { first . valid } } < / p >
< p > Form value : { { f . value | json } } < / p >
< p > Form valid : { { f . valid } } < / p >
` ,
'Identifier \'valwe\' is not defined' ) ) ;
it ( 'should accept a call to a method' , ( ) = > accept ( '{{getPerson().name.first}}' ) ) ;
it ( 'should reject a misspelled field of a method result' ,
( ) = > reject ( '{{getPerson().nume.first}}' , 'Identifier \'nume\' is not defined' ) ) ;
it ( 'should reject calling a uncallable member' ,
2020-02-25 11:32:38 -05:00
( ) = > reject ( '{{person().name.first}}' , '\'person\' is not callable' ) ) ;
2017-05-09 19:16:50 -04:00
it ( 'should accept an event handler' ,
( ) = > accept ( '<div (click)="click($event)">{{person.name.first}}</div>' ) ) ;
it ( 'should reject a misspelled event handler' ,
( ) = > reject (
2020-02-06 17:49:49 -05:00
'<div (click)="clack($event)">{{person.name.first}}</div>' ,
` Identifier 'clack' is not defined. The component declaration, template variable declarations, and element references do not contain such a member ` ) ) ;
2017-05-09 19:16:50 -04:00
it ( 'should reject an uncalled event handler' ,
( ) = > reject (
'<div (click)="click">{{person.name.first}}</div>' , 'Unexpected callable expression' ) ) ;
2018-03-10 12:14:58 -05:00
describe ( 'with comparisons between nullable and non-nullable' , ( ) = > {
2017-05-16 19:36:51 -04:00
it ( 'should accept ==' , ( ) = > accept ( ` <div>{{e == 1 ? 'a' : 'b'}}</div> ` ) ) ;
it ( 'should accept ===' , ( ) = > accept ( ` <div>{{e === 1 ? 'a' : 'b'}}</div> ` ) ) ;
it ( 'should accept !=' , ( ) = > accept ( ` <div>{{e != 1 ? 'a' : 'b'}}</div> ` ) ) ;
it ( 'should accept !==' , ( ) = > accept ( ` <div>{{e !== 1 ? 'a' : 'b'}}</div> ` ) ) ;
it ( 'should accept &&' , ( ) = > accept ( ` <div>{{e && 1 ? 'a' : 'b'}}</div> ` ) ) ;
it ( 'should accept ||' , ( ) = > accept ( ` <div>{{e || 1 ? 'a' : 'b'}}</div> ` ) ) ;
it ( 'should reject >' ,
( ) = > reject ( ` <div>{{e > 1 ? 'a' : 'b'}}</div> ` , 'The expression might be null' ) ) ;
} ) ;
2017-05-09 19:16:50 -04:00
} ) ;
const FILES : Directory = {
'src' : {
'app' : {
'app.component.ts' : `
import { Component , NgModule } from '@angular/core' ;
import { CommonModule } from '@angular/common' ;
import { FormsModule } from '@angular/forms' ;
export interface Person {
name : Name ;
address : Address ;
}
export interface Name {
first : string ;
middle : string ;
last : string ;
}
export interface Address {
street : string ;
city : string ;
state : string ;
zip : string ;
}
@Component ( {
selector : 'my-app' ,
templateUrl : './app.component.html'
} )
export class AppComponent {
person : Person ;
people : Person [ ] ;
maybe_person? : Person ;
promised_person : Promise < Person > ;
promised_people : Promise < Person [ ] > ;
private private_person : Person ;
private private_people : Person [ ] ;
2017-05-16 19:36:51 -04:00
e? : number ;
2017-05-09 19:16:50 -04:00
getPerson ( ) : Person { return this . person ; }
click() { }
}
@NgModule ( {
imports : [ CommonModule , FormsModule ] ,
declarations : [ AppComponent ]
} )
export class AppModule { }
`
}
}
2017-10-24 07:54:08 -04:00
} ;