From 5f76de1d71ac40654b5eef2d58747a713aab5c06 Mon Sep 17 00:00:00 2001 From: Keen Yee Liau Date: Thu, 15 Aug 2019 15:17:00 -0700 Subject: [PATCH] test(language-service): Fix diagnostic tests (#32161) This commit fixes many diagnostic tests have have incorrect/uncaught assertions. Also added more assertions to make sure TS diagnostics are clear. A few test util methods are removed to reduce clutter and improve readability. PR Close #32161 --- .../language-service/test/diagnostics_spec.ts | 823 ++++++++++-------- packages/language-service/test/test_utils.ts | 53 +- 2 files changed, 457 insertions(+), 419 deletions(-) diff --git a/packages/language-service/test/diagnostics_spec.ts b/packages/language-service/test/diagnostics_spec.ts index 823bcad12b..82ea25bb30 100644 --- a/packages/language-service/test/diagnostics_spec.ts +++ b/packages/language-service/test/diagnostics_spec.ts @@ -7,393 +7,480 @@ */ import * as ts from 'typescript'; - import {createLanguageService} from '../src/language_service'; -import {Diagnostics, LanguageService} from '../src/types'; +import * as ng from '../src/types'; import {TypeScriptServiceHost} from '../src/typescript_host'; - import {toh} from './test_data'; -import {MockTypescriptHost, diagnosticMessageContains, findDiagnostic, includeDiagnostic, noDiagnostics} from './test_utils'; +import {MockTypescriptHost} from './test_utils'; + +/** + * Note: If we want to test that a specific diagnostic message is emitted, then + * use the `addCode()` helper method to add code to an existing file and check + * that the diagnostic messages contain the expected output. + * + * If the goal is to assert that there is no error in a specific file, then use + * `mockHost.override()` method to completely override an existing file, and + * make sure no diagnostics are produced. When doing so, be extra cautious + * about import statements and make sure to assert empty TS diagnostic messages + * as well. + */ describe('diagnostics', () => { let mockHost: MockTypescriptHost; let ngHost: TypeScriptServiceHost; - let ngService: LanguageService; + let tsLS: ts.LanguageService; + let ngLS: ng.LanguageService; beforeEach(() => { mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh); - const documentRegistry = ts.createDocumentRegistry(); - const service = ts.createLanguageService(mockHost, documentRegistry); - ngHost = new TypeScriptServiceHost(mockHost, service); - ngService = createLanguageService(ngHost); + tsLS = ts.createLanguageService(mockHost); + ngHost = new TypeScriptServiceHost(mockHost, tsLS); + ngLS = createLanguageService(ngHost); }); - it('should be no diagnostics for test.ng', - () => { expect(ngService.getDiagnostics('/app/test.ng')).toEqual([]); }); + it('should produce no diagnostics for test.ng', () => { + // there should not be any errors on existing external template + expect(ngLS.getDiagnostics('/app/test.ng')).toEqual([]); + }); - describe('for semantic errors', () => { + it('should not return TS and NG errors for existing files', () => { + const files = [ + '/app/app.component.ts', + '/app/main.ts', + ]; + for (const file of files) { + const syntaxDiags = tsLS.getSyntacticDiagnostics(file); + expect(syntaxDiags).toEqual([]); + const semanticDiags = tsLS.getSemanticDiagnostics(file); + expect(semanticDiags).toEqual([]); + const ngDiags = ngLS.getDiagnostics(file); + expect(ngDiags).toEqual([]); + } + }); + + // #17611 + it('should not report diagnostic on iteration of any', () => { const fileName = '/app/test.ng'; - - function diagnostics(template: string): ts.Diagnostic[] { - try { - mockHost.override(fileName, template); - return ngService.getDiagnostics(fileName) !; - } finally { - mockHost.override(fileName, undefined !); - } - } - - function accept(template: string) { noDiagnostics(diagnostics(template)); } - - function reject(template: string, message: string): void; - function reject(template: string, message: string, at: string): void; - function reject(template: string, message: string, location: string): void; - function reject(template: string, message: string, location: string, len: number): void; - function reject(template: string, message: string, at?: number | string, len?: number): void { - if (typeof at == 'string') { - len = at.length; - at = template.indexOf(at); - } - includeDiagnostic(diagnostics(template), message, at, len); - } - - describe('regression', () => { - it('should be able to return diagnostics if reflector gets invalidated', () => { - const fileName = '/app/main.ts'; - ngService.getDiagnostics(fileName); - (ngHost as any)._reflector = null; - ngService.getDiagnostics(fileName); - }); - - // #17611 - it('should not report diagnostic on iteration of any', - () => { accept('
{{value.someField}}
'); }); - }); - - describe('with $event', () => { - it('should accept an event', - () => { accept('
Click me!
'); }); - it('should reject it when not in an event binding', () => { - reject('
', '\'$event\' is not defined', '$event'); - }); - }); + mockHost.override(fileName, '
{{value.someField}}
'); + const diagnostics = ngLS.getDiagnostics(fileName); + expect(diagnostics).toEqual([]); }); - describe('with regression tests', () => { - - it('should not crash with a incomplete *ngFor', () => { - expect(() => { - const code = - '\n@Component({template: \'
~{after-div}\'}) export class MyComponent {}'; - addCode(code, fileName => { ngService.getDiagnostics(fileName); }); - }).not.toThrow(); - }); - - it('should report a component not in a module', () => { - const code = '\n@Component({template: \'
\'}) export class MyComponent {}'; - addCode(code, (fileName, content) => { - const diagnostics = ngService.getDiagnostics(fileName); - const offset = content !.lastIndexOf('@Component') + 1; - const len = 'Component'.length; - includeDiagnostic( - diagnostics !, 'Component \'MyComponent\' is not included in a module', offset, len); - }); - }); - - it('should not report an error for a form\'s host directives', () => { - const code = '\n@Component({template: \'
\'}) export class MyComponent {}'; - addCode(code, fileName => { - const diagnostics = ngService.getDiagnostics(fileName); - expectOnlyModuleDiagnostics(diagnostics); - }); - }); - - it('should not throw getting diagnostics for an index expression', () => { - const code = - ` @Component({template: ''}) export class MyComponent {}`; - addCode( - code, fileName => { expect(() => ngService.getDiagnostics(fileName)).not.toThrow(); }); - }); - - it('should not throw using a directive with no value', () => { - const code = - ` @Component({template: '
'}) export class MyComponent { name = 'some name'; }`; - addCode( - code, fileName => { expect(() => ngService.getDiagnostics(fileName)).not.toThrow(); }); - }); - - it('should report an error for invalid metadata', () => { - const code = - ` @Component({template: '', provider: [{provide: 'foo', useFactor: () => 'foo' }]}) export class MyComponent { name = 'some name'; }`; - addCode(code, (fileName, content) => { - const diagnostics = ngService.getDiagnostics(fileName); - includeDiagnostic( - diagnostics !, 'Function expressions are not supported in decorators', '() => \'foo\'', - content); - }); - }); - - it('should not throw for an invalid class', () => { - const code = ` @Component({template: ''}) class`; - addCode( - code, fileName => { expect(() => ngService.getDiagnostics(fileName)).not.toThrow(); }); - }); - - it('should not report an error for sub-types of string', () => { - const code = - ` @Component({template: \`
\`}) export class MyComponent { something: 'foo' | 'bar'; }`; - addCode(code, fileName => { - const diagnostics = ngService.getDiagnostics(fileName); - expectOnlyModuleDiagnostics(diagnostics); - }); - }); - - it('should not report an error for sub-types of number', () => { - const code = - ` @Component({template: \`
\`}) export class MyComponent { something: 123 | 456; }`; - addCode(code, fileName => { - const diagnostics = ngService.getDiagnostics(fileName); - expectOnlyModuleDiagnostics(diagnostics); - }); - }); - - it('should report a warning if an event results in a callable expression', () => { - const code = - ` @Component({template: \`
\`}) export class MyComponent { onClick() { } }`; - addCode(code, (fileName, content) => { - const diagnostics = ngService.getDiagnostics(fileName); - includeDiagnostic( - diagnostics !, 'Unexpected callable expression. Expected a method call', 'onClick', - content); - }); - }); - - // #13412 - it('should not report an error for using undefined', () => { - const code = - ` @Component({template: \`
\`}) export class MyComponent { something = 'foo'; }})`; - addCode(code, fileName => { - const diagnostics = ngService.getDiagnostics(fileName); - expectOnlyModuleDiagnostics(diagnostics); - }); - }); - - // Issue #13326 - it('should report a narrow span for invalid pipes', () => { - const code = - ` @Component({template: '

Using an invalid pipe {{data | dat}}

'}) export class MyComponent { data = 'some data'; }`; - addCode(code, fileName => { - const diagnostic = findDiagnostic(ngService.getDiagnostics(fileName) !, 'pipe') !; - expect(diagnostic).not.toBeUndefined(); - expect(diagnostic.length).toBeLessThan(11); - }); - }); - - // Issue #19406 - it('should allow empty template', () => { - const appComponent = ` - import { Component } from '@angular/core'; - - @Component({ - template : '', - }) - export class AppComponent {} - `; - const fileName = '/app/app.component.ts'; - mockHost.override(fileName, appComponent); - const diagnostics = ngService.getDiagnostics(fileName); + describe('with $event', () => { + it('should accept an event', () => { + const fileName = '/app/test.ng'; + mockHost.override(fileName, '
Click me!
'); + const diagnostics = ngLS.getDiagnostics(fileName); expect(diagnostics).toEqual([]); }); - // Issue #15460 - it('should be able to find members defined on an ancestor type', () => { - const app_component = ` - import { Component } from '@angular/core'; - import { NgForm } from '@angular/common'; - - @Component({ - selector: 'example-app', - template: \` -
- - - -
-

First name value: {{ first.value }}

-

First name valid: {{ first.valid }}

-

Form value: {{ f.value | json }}

-

Form valid: {{ f.valid }}

- \`, - }) - export class AppComponent { - onSubmit(form: NgForm) {} - } - `; - const fileName = '/app/app.component.ts'; - mockHost.override(fileName, app_component); - const diagnostic = ngService.getDiagnostics(fileName); - expect(diagnostic).toEqual([]); - }); - - it('should report an error for invalid providers', () => { - addCode( - ` - @Component({ - template: '', - providers: [null] - }) - export class MyComponent {} - `, - fileName => { - const diagnostics = ngService.getDiagnostics(fileName) !; - const expected = findDiagnostic(diagnostics, 'Invalid providers for'); - const notExpected = findDiagnostic(diagnostics, 'Cannot read property'); - expect(expected).toBeDefined(); - expect(notExpected).toBeUndefined(); - }); - }); - - // Issue #15768 - it('should be able to parse a template reference', () => { - addCode( - ` - @Component({ - selector: 'my-component', - template: \` -
-
- Loading comps... - \` - }) - export class MyComponent {} - `, - fileName => expectOnlyModuleDiagnostics(ngService.getDiagnostics(fileName))); - }); - - // Issue #15625 - it('should not report errors for localization syntax', () => { - addCode( - ` - @Component({ - selector: 'my-component', - template: \` -
- {fieldCount, plural, =0 {no fields} =1 {1 field} other {{{fieldCount}} fields}} -
- \` - }) - export class MyComponent { - fieldCount: number; - } - `, - fileName => { - const diagnostics = ngService.getDiagnostics(fileName); - expectOnlyModuleDiagnostics(diagnostics); - }); - }); - - // Issue #15885 - it('should be able to remove null and undefined from a type', () => { - mockHost.overrideOptions(options => { - options.strictNullChecks = true; - return options; - }); - addCode( - ` - @Component({ - selector: 'my-component', - template: \` {{test?.a}} - \` - }) - export class MyComponent { - test: {a: number, b: number} | null = { - a: 1, - b: 2 - }; - } - `, - fileName => expectOnlyModuleDiagnostics(ngService.getDiagnostics(fileName))); - }); - - it('should be able to resolve modules using baseUrl', () => { - const app_component = ` - import { Component } from '@angular/core'; - import { NgForm } from '@angular/common'; - import { Server } from 'app/server'; - - @Component({ - selector: 'example-app', - template: '...', - providers: [Server] - }) - export class AppComponent { - onSubmit(form: NgForm) {} - } - `; - const app_server = ` - export class Server {} - `; - const fileName = '/app/app.component.ts'; - mockHost.override(fileName, app_component); - mockHost.addScript('/other/files/app/server.ts', app_server); - mockHost.overrideOptions(options => { - options.baseUrl = '/other/files'; - return options; - }); - const diagnostic = ngService.getDiagnostics(fileName); - expect(diagnostic).toEqual([]); - }); - - it('should not report errors for using the now removed OpaqueToken (support for v4)', () => { - const app_component = ` - import { Component, Inject, OpaqueToken } from '@angular/core'; - import { NgForm } from '@angular/common'; - - export const token = new OpaqueToken(); - - @Component({ - selector: 'example-app', - template: '...' - }) - export class AppComponent { - constructor (@Inject(token) value: string) {} - onSubmit(form: NgForm) {} - } - `; - const fileName = '/app/app.component.ts'; - mockHost.override(fileName, app_component); - const diagnostics = ngService.getDiagnostics(fileName); - expect(diagnostics).toEqual([]); - }); - - 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); - ngHost.getAnalyzedModules(); - try { - cb(fileName, newContent); - } finally { - mockHost.override(fileName, undefined !); - } - } - - function expectOnlyModuleDiagnostics(diagnostics: ts.Diagnostic[] | undefined) { - // Expect only the 'MyComponent' diagnostic - if (!diagnostics) throw new Error('Expecting Diagnostics'); - if (diagnostics.length > 1) { - const unexpectedDiagnostics = - diagnostics.filter(diag => !diagnosticMessageContains(diag.messageText, 'MyComponent')) - .map(diag => `(${diag.start}:${diag.start! + diag.length!}): ${diag.messageText}`); - - if (unexpectedDiagnostics.length) { - fail(`Unexpected diagnostics:\n ${unexpectedDiagnostics.join('\n ')}`); - return; - } - } + it('should reject it when not in an event binding', () => { + const fileName = '/app/test.ng'; + const content = mockHost.override(fileName, '
'); + const diagnostics = ngLS.getDiagnostics(fileName) !; expect(diagnostics.length).toBe(1); - expect(diagnosticMessageContains(diagnostics[0].messageText, 'MyComponent')).toBeTruthy(); - } + const {messageText, start, length} = diagnostics[0]; + expect(messageText) + .toBe( + 'Identifier \'$event\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member'); + const keyword = '$event'; + expect(start).toBe(content.lastIndexOf(keyword)); + expect(length).toBe(keyword.length); + }); }); + + it('should not crash with a incomplete *ngFor', () => { + const fileName = addCode(` + @Component({ + template: '
~{after-div}' + }) + export class MyComponent {}`); + expect(() => ngLS.getDiagnostics(fileName)).not.toThrow(); + }); + + it('should report a component not in a module', () => { + const fileName = addCode(` + @Component({ + template: '
' + }) + export class MyComponent {}`); + const diagnostics = ngLS.getDiagnostics(fileName) !; + expect(diagnostics.length).toBe(1); + const {messageText, start, length} = diagnostics[0]; + expect(messageText) + .toBe( + 'Component \'MyComponent\' is not included in a module and will not be available inside a template. Consider adding it to a NgModule declaration.'); + const content = mockHost.getFileContent(fileName) !; + const keyword = '@Component'; + expect(start).toBe(content.lastIndexOf(keyword) + 1); // exclude leading '@' + expect(length).toBe(keyword.length - 1); // exclude leading '@' + }); + + + it(`should not report an error for a form's host directives`, () => { + const fileName = '/app/app.component.ts'; + mockHost.override(fileName, ` + import { Component } from '@angular/core'; + + @Component({ + template: '
'}) + export class AppComponent {}`); + const tsDiags = tsLS.getSemanticDiagnostics(fileName); + expect(tsDiags).toEqual([]); + const ngDiags = ngLS.getDiagnostics(fileName); + expect(ngDiags).toEqual([]); + }); + + it('should not throw getting diagnostics for an index expression', () => { + const fileName = addCode(` + @Component({ + template: '' + }) + export class MyComponent {}`); + expect(() => ngLS.getDiagnostics(fileName)).not.toThrow(); + }); + + it('should not throw using a directive with no value', () => { + const fileName = addCode(` + @Component({ + template: '
' + }) + export class MyComponent { + name = 'some name'; + }`); + expect(() => ngLS.getDiagnostics(fileName)).not.toThrow(); + }); + + it('should report an error for invalid metadata', () => { + const fileName = '/app/app.component.ts'; + const content = mockHost.override(fileName, ` + import { Component } from '@angular/core'; + + @Component({ + template: '
', + providers: [ + {provide: 'foo', useFactory: () => 'foo' } + ] + }) + export class AppComponent { + name = 'some name'; + }`); + const tsDiags = tsLS.getSemanticDiagnostics(fileName); + expect(tsDiags).toEqual([]); + const ngDiags = ngLS.getDiagnostics(fileName) !; + expect(ngDiags.length).toBe(1); + const {messageText, start, length} = ngDiags[0]; + const keyword = `() => 'foo'`; + expect(start).toBe(content.lastIndexOf(keyword)); + expect(length).toBe(keyword.length); + // messageText is a three-part chain + const firstPart = messageText as ts.DiagnosticMessageChain; + expect(firstPart.messageText).toBe('Error during template compile of \'AppComponent\''); + const secondPart = firstPart.next !; + expect(secondPart.messageText).toBe('Function expressions are not supported in decorators'); + const thirdPart = secondPart.next !; + expect(thirdPart.messageText) + .toBe('Consider changing the function expression into an exported function'); + expect(thirdPart.next).toBeFalsy(); + }); + + it('should not throw for an invalid class', () => { + const fileName = addCode(` + @Component({ + template: '' + }) class`); + expect(() => ngLS.getDiagnostics(fileName)).not.toThrow(); + }); + + it('should not report an error for sub-types of string', () => { + const fileName = '/app/app.component.ts'; + mockHost.override(fileName, ` + import { Component } from '@angular/core'; + + @Component({ + template: \`
\` + }) + export class AppComponent { + something: 'foo' | 'bar'; + }`); + const tsDiags = tsLS.getSemanticDiagnostics(fileName); + expect(tsDiags).toEqual([]); + const ngDiags = ngLS.getDiagnostics(fileName); + expect(ngDiags).toEqual([]); + }); + + it('should not report an error for sub-types of number', () => { + const fileName = '/app/app.component.ts'; + mockHost.override(fileName, ` + import { Component } from '@angular/core'; + + @Component({ + template: '
' + }) + export class AppComponent { + something: 123 | 456; + }`); + const tsDiags = tsLS.getSemanticDiagnostics(fileName); + expect(tsDiags).toEqual([]); + const ngDiags = ngLS.getDiagnostics(fileName); + expect(ngDiags).toEqual([]); + }); + + it('should report a warning if an event results in a callable expression', () => { + const fileName = '/app/app.component.ts'; + const content = mockHost.override(fileName, ` + import { Component } from '@angular/core'; + + @Component({ + template: '
' + }) + export class MyComponent { + onClick() { } + }`); + const diagnostics = ngLS.getDiagnostics(fileName) !; + const {messageText, start, length} = diagnostics[0]; + expect(messageText).toBe('Unexpected callable expression. Expected a method call'); + const keyword = `"onClick"`; + expect(start).toBe(content.lastIndexOf(keyword) + 1); // exclude leading quote + expect(length).toBe(keyword.length - 2); // exclude leading and trailing quotes + }); + + // #13412 + it('should not report an error for using undefined', () => { + const fileName = '/app/app.component.ts'; + mockHost.override(fileName, ` + import { Component } from '@angular/core'; + + @Component({ + template: '
' + }) + export class AppComponent { + something = 'foo'; + }`); + const tsDiags = tsLS.getSemanticDiagnostics(fileName); + expect(tsDiags).toEqual([]); + const ngDiags = ngLS.getDiagnostics(fileName); + expect(ngDiags).toEqual([]); + }); + + // Issue #13326 + it('should report a narrow span for invalid pipes', () => { + const fileName = '/app/app.component.ts'; + const content = mockHost.override(fileName, ` + import { Component } from '@angular/core'; + + @Component({ + template: '

Using an invalid pipe {{data | dat}}

' + }) + export class AppComponent { + data = 'some data'; + }`); + const tsDiags = tsLS.getSemanticDiagnostics(fileName); + expect(tsDiags).toEqual([]); + const ngDiags = ngLS.getDiagnostics(fileName); + expect(ngDiags.length).toBe(1); + const {messageText, start, length} = ngDiags[0]; + expect(messageText).toBe(`The pipe 'dat' could not be found`); + const keyword = 'data | dat'; + expect(start).toBe(content.lastIndexOf(keyword)); + expect(length).toBe(keyword.length); + }); + + // Issue #19406 + it('should allow empty template', () => { + const fileName = '/app/app.component.ts'; + mockHost.override(fileName, ` + import { Component } from '@angular/core'; + + @Component({ + template : '', + }) + export class AppComponent {}`); + const tsDiags = tsLS.getSemanticDiagnostics(fileName); + expect(tsDiags).toEqual([]); + const ngDiags = ngLS.getDiagnostics(fileName); + expect(ngDiags).toEqual([]); + }); + + // Issue #15460 + it('should be able to find members defined on an ancestor type', () => { + const fileName = '/app/app.component.ts'; + mockHost.override(fileName, ` + import { Component } from '@angular/core'; + import { NgForm } from '@angular/forms'; + + @Component({ + selector: 'example-app', + template: \` +
+ + + +
+

First name value: {{ first.value }}

+

First name valid: {{ first.valid }}

+

Form value: {{ f.value | json }}

+

Form valid: {{ f.valid }}

+ \`, + }) + export class AppComponent { + onSubmit(form: NgForm) {} + }`); + const tsDiags = tsLS.getSemanticDiagnostics(fileName); + expect(tsDiags).toEqual([]); + const ngDiags = ngLS.getDiagnostics(fileName); + expect(ngDiags).toEqual([]); + }); + + it('should report an error for invalid providers', () => { + const fileName = '/app/app.component.ts'; + const content = mockHost.override(fileName, ` + import { Component } from '@angular/core'; + + @Component({ + template: '', + providers: [null] + }) + export class AppComponent {}`); + const tsDiags = tsLS.getSemanticDiagnostics(fileName); + expect(tsDiags).toEqual([]); + const ngDiags = ngLS.getDiagnostics(fileName); + expect(ngDiags.length).toBe(1); + const {messageText, start, length} = ngDiags[0]; + expect(messageText) + .toBe( + 'Invalid providers for "AppComponent in /app/app.component.ts" - only instances of Provider and Type are allowed, got: [?null?]'); + // TODO: Looks like this is the wrong span. Should point to 'null' instead. + const keyword = '@Component'; + expect(start).toBe(content.lastIndexOf(keyword) + 1); // exclude leading '@' + expect(length).toBe(keyword.length - 1); // exclude leading '@ + }); + + // Issue #15768 + it('should be able to parse a template reference', () => { + const fileName = '/app/app.component.ts'; + mockHost.override(fileName, ` + import { Component } from '@angular/core'; + + @Component({ + selector: 'my-component', + template: \` +
+
+ Loading comps... + \` + }) + export class AppComponent {}`); + const tsDiags = tsLS.getSemanticDiagnostics(fileName); + expect(tsDiags).toEqual([]); + const ngDiags = ngLS.getDiagnostics(fileName); + expect(ngDiags).toEqual([]); + }); + + // Issue #15625 + it('should not report errors for localization syntax', () => { + const fileName = '/app/app.component.ts'; + mockHost.override(fileName, ` + import { Component } from '@angular/core'; + + @Component({ + selector: 'my-component', + template: \` +
+ {fieldCount, plural, =0 {no fields} =1 {1 field} other {{{fieldCount}} fields}} +
+ \` + }) + export class AppComponent { + fieldCount: number; + }`); + const tsDiags = tsLS.getSemanticDiagnostics(fileName); + expect(tsDiags).toEqual([]); + const ngDiags = ngLS.getDiagnostics(fileName); + expect(ngDiags).toEqual([]); + }); + + // Issue #15885 + it('should be able to remove null and undefined from a type', () => { + mockHost.overrideOptions(options => { + options.strictNullChecks = true; + return options; + }); + const fileName = '/app/app.component.ts'; + mockHost.override(fileName, ` + import { Component } from '@angular/core'; + + @Component({ + selector: 'my-component', + template: '{{test?.a}}', + }) + export class AppComponent { + test: {a: number, b: number} | null = { + a: 1, + b: 2, + }; + }`); + const tsDiags = tsLS.getSemanticDiagnostics(fileName); + expect(tsDiags).toEqual([]); + const ngDiags = ngLS.getDiagnostics(fileName); + expect(ngDiags).toEqual([]); + }); + + it('should be able to resolve modules using baseUrl', () => { + const fileName = '/app/app.component.ts'; + mockHost.override(fileName, ` + import { Component } from '@angular/core'; + import { NgForm } from '@angular/forms'; + import { Server } from 'app/server'; + + @Component({ + selector: 'example-app', + template: '...', + providers: [Server] + }) + export class AppComponent { + onSubmit(form: NgForm) {} + }`); + mockHost.addScript('/other/files/app/server.ts', 'export class Server {}'); + mockHost.overrideOptions(options => { + options.baseUrl = '/other/files'; + return options; + }); + const tsDiags = tsLS.getSemanticDiagnostics(fileName); + expect(tsDiags).toEqual([]); + const diagnostic = ngLS.getDiagnostics(fileName); + expect(diagnostic).toEqual([]); + }); + + it('should report errors for using the now removed OpaqueToken (deprecated)', () => { + const fileName = '/app/app.component.ts'; + mockHost.override(fileName, ` + import { Component, Inject, OpaqueToken } from '@angular/core'; + import { NgForm } from '@angular/forms'; + + export const token = new OpaqueToken('some token'); + + @Component({ + selector: 'example-app', + template: '...' + }) + export class AppComponent { + constructor (@Inject(token) value: string) {} + onSubmit(form: NgForm) {} + }`); + const tsDiags = tsLS.getSemanticDiagnostics(fileName); + expect(tsDiags.length).toBe(1); + expect(tsDiags[0].messageText) + .toBe( + `Module '"../node_modules/@angular/core/core"' has no exported member 'OpaqueToken'.`); + }); + + function addCode(code: string) { + const fileName = '/app/app.component.ts'; + const originalContent = mockHost.getFileContent(fileName); + const newContent = originalContent + code; + mockHost.override(fileName, newContent); + return fileName; + } + }); diff --git a/packages/language-service/test/test_utils.ts b/packages/language-service/test/test_utils.ts index 80dcddadb9..405ebbdcf5 100644 --- a/packages/language-service/test/test_utils.ts +++ b/packages/language-service/test/test_utils.ts @@ -11,7 +11,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; -import {Diagnostic, DiagnosticMessageChain, Diagnostics, Span} from '../src/types'; +import {Span} from '../src/types'; export type MockData = string | MockDirectory; @@ -104,6 +104,7 @@ export class MockTypescriptHost implements ts.LanguageServiceHost { } else { this.overrides.delete(fileName); } + return content; } addScript(fileName: string, content: string) { @@ -330,53 +331,3 @@ function getReferenceMarkers(value: string): ReferenceResult { function removeReferenceMarkers(value: string): string { return value.replace(referenceMarker, (match, text) => text.replace(/ᐱ/g, '')); } - -export function noDiagnostics(diagnostics: ts.Diagnostic[]) { - if (diagnostics && diagnostics.length) { - throw new Error( - `Unexpected diagnostics: \n ${diagnostics.map(d => d.messageText).join('\n ')}`); - } -} - -export function diagnosticMessageContains( - message: string | ts.DiagnosticMessageChain, messageFragment: string): boolean { - if (typeof message == 'string') { - return message.indexOf(messageFragment) >= 0; - } - if (message.messageText.indexOf(messageFragment) >= 0) { - return true; - } - if (message.next) { - return diagnosticMessageContains(message.next, messageFragment); - } - return false; -} - -export function findDiagnostic( - diagnostics: ts.Diagnostic[], messageFragment: string): ts.Diagnostic|undefined { - return diagnostics.find(d => diagnosticMessageContains(d.messageText, messageFragment)); -} - -export function includeDiagnostic( - diagnostics: ts.Diagnostic[], message: string, text?: string, len?: string): void; -export function includeDiagnostic( - diagnostics: ts.Diagnostic[], message: string, at?: number, len?: number): void; -export function includeDiagnostic( - diagnostics: ts.Diagnostic[], message: string, p1?: any, p2?: any) { - expect(diagnostics).toBeDefined(); - if (diagnostics) { - const diagnostic = findDiagnostic(diagnostics, message); - expect(diagnostic).toBeDefined(`no diagnostic contains '${message}`); - if (diagnostic && p1 != null) { - const at = typeof p1 === 'number' ? p1 : p2.indexOf(p1); - const len = typeof p2 === 'number' ? p2 : p1.length; - expect(diagnostic.start) - .toEqual( - at, - `expected message '${message}' was reported at ${diagnostic.start} but should be ${at}`); - if (len != null) { - expect(diagnostic.length).toEqual(len, `expected '${message}'s span length to be ${len}`); - } - } - } -}